mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-22 23:24:03 +08:00
Compare commits
189
Commits
v2.0.0
...
v2.0.0-alpha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
+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.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
    [](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/3e3ec45b195c92989434fde40ae110027f4ea124/misc/icons/icon.png" alt="HA Text AI" width="140"/>
|
||||||
|
|
||||||
|
### Advanced AI Integration for Home Assistant with multi-provider support
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -11,6 +14,10 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
> [!NOTE]
|
||||||
|
> 🚧 ALPHA VERSION 🚧
|
||||||
|
> Expect: potential bugs, frequent changes, incomplete features.
|
||||||
|
> 🤝 Community Driven
|
||||||
|
|
||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
@@ -19,36 +26,43 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
|||||||
- Anthropic Claude integration
|
- Anthropic Claude integration
|
||||||
- Custom API endpoints
|
- Custom API endpoints
|
||||||
- Flexible model selection
|
- Flexible model selection
|
||||||
|
|
||||||
- 💬 **Advanced Language Processing**:
|
- 💬 **Advanced Language Processing**:
|
||||||
- Context-aware responses
|
- Context-aware responses
|
||||||
- Multi-turn conversations
|
- Multi-turn conversations
|
||||||
- Custom system instructions
|
- Custom system instructions
|
||||||
- Natural conversation flow
|
- Natural conversation flow
|
||||||
|
|
||||||
- 📝 **Enhanced Memory Management**:
|
- 📝 **Enhanced Memory Management**:
|
||||||
- Persistent conversation history
|
- Persistent conversation history
|
||||||
- Context-aware responses
|
- Context-aware responses
|
||||||
- Customizable history limits
|
- Customizable history limits
|
||||||
- Model-specific filtering
|
- Model-specific filtering
|
||||||
|
|
||||||
- ⚡ **Performance Optimization**:
|
- ⚡ **Performance Optimization**:
|
||||||
- Efficient token usage
|
- Efficient token usage
|
||||||
- Smart rate limiting
|
- Smart rate limiting
|
||||||
- Response caching
|
- Response caching
|
||||||
- Request interval control
|
- Request interval control
|
||||||
|
|
||||||
- 🎯 **Advanced Customization**:
|
- 🎯 **Advanced Customization**:
|
||||||
- Per-request model selection
|
- Per-request model selection
|
||||||
- Adjustable parameters
|
- Adjustable parameters
|
||||||
- Custom system prompts
|
- Custom system prompts
|
||||||
- Temperature control
|
- Temperature control
|
||||||
|
|
||||||
- 🔒 **Enhanced Security**:
|
- 🔒 **Enhanced Security**:
|
||||||
- Secure API key storage
|
- Secure API key storage
|
||||||
- Rate limiting protection
|
- Rate limiting protection
|
||||||
- Error handling
|
- Error handling
|
||||||
- Usage monitoring
|
- Usage monitoring
|
||||||
|
|
||||||
- 🎨 **Improved User Experience**:
|
- 🎨 **Improved User Experience**:
|
||||||
- Intuitive configuration UI
|
- Intuitive configuration UI
|
||||||
- Detailed sensor attributes
|
- Detailed sensor attributes
|
||||||
- Rich service interface
|
- Rich service interface
|
||||||
- Model selection UI
|
- Model selection UI
|
||||||
|
|
||||||
- 🔄 **Automation Integration**:
|
- 🔄 **Automation Integration**:
|
||||||
- Event-driven responses
|
- Event-driven responses
|
||||||
- Conditional logic support
|
- Conditional logic support
|
||||||
@@ -57,25 +71,58 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
|||||||
|
|
||||||
## 📋 Prerequisites
|
## 📋 Prerequisites
|
||||||
|
|
||||||
- Home Assistant 2023.8.0 or newer
|
- Home Assistant 2023.11 or later
|
||||||
- API key from supported providers:
|
- Active API key from:
|
||||||
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
|
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
|
||||||
- Anthropic ([Get key](https://console.anthropic.com/))
|
- Anthropic ([Get key](https://console.anthropic.com/))
|
||||||
|
- OpenRouter ([Get key](https://openrouter.ai/keys))
|
||||||
- Python 3.9 or newer
|
- Python 3.9 or newer
|
||||||
- Stable internet connection
|
- Stable internet connection
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
- API Provider (OpenAI/Anthropic)
|
||||||
|
- API Key (provider-specific)
|
||||||
|
- Model Selection (flexible, provider-specific models)
|
||||||
|
- Temperature (Creativity control, 0.0-2.0)
|
||||||
|
- Max Tokens (Response length limit)
|
||||||
|
- Request Interval (API call throttling)
|
||||||
|
- Custom API Endpoint (optional)
|
||||||
|
|
||||||
|
#### ⓘ Potentially Compatible Providers
|
||||||
|
The integration is designed to be flexible and may work with other providers offering OpenAI-compatible APIs:
|
||||||
|
- Groq
|
||||||
|
- Together AI
|
||||||
|
- Perplexity AI
|
||||||
|
- Mistral AI
|
||||||
|
- Google AI
|
||||||
|
- Local AI servers (like Ollama)
|
||||||
|
- Custom OpenAI-compatible endpoints
|
||||||
|
|
||||||
|
#### Additional Notes
|
||||||
|
- Not all providers guarantee full compatibility
|
||||||
|
- Performance may vary between providers
|
||||||
|
- Check individual provider's documentation
|
||||||
|
- Ensure your API key has sufficient credits/quota
|
||||||
|
|
||||||
|
#### Provider Compatibility Requirements
|
||||||
|
To be compatible, a provider should support:
|
||||||
|
- OpenAI-like REST API structure
|
||||||
|
- JSON request/response format
|
||||||
|
- Standard authentication method
|
||||||
|
- Similar model parameter handling
|
||||||
|
|
||||||
## ⚡ Installation
|
## ⚡ Installation
|
||||||
|
|
||||||
### HACS Installation (Recommended)
|
### HACS Installation (Recommended)
|
||||||
|
<a href="https://my.home-assistant.io/redirect/hacs_repository/?owner=smkrv&repository=ha-text-ai&category=Integration"><img src="https://my.home-assistant.io/badges/hacs_repository.svg" width="170" height="auto"></a>
|
||||||
1. Open HACS in Home Assistant
|
1. Open HACS in Home Assistant
|
||||||
2. Click on the three dots in the top right corner
|
2. Click on "Integrations"
|
||||||
3. Select "Custom repositories"
|
3. Click "..." in top right corner
|
||||||
4. Add `https://github.com/smkrv/ha-text-ai` as Integration
|
4. Select "Custom repositories"
|
||||||
5. Click "Add"
|
5. Add repository URL: `https://github.com/smkrv/ha-text-ai`
|
||||||
6. Click on "Integrations" in HACS
|
6. Choose "Integration" as category
|
||||||
7. Search for "HA Text AI"
|
7. Click "Download"
|
||||||
8. Click "Download"
|
8. Restart Home Assistant
|
||||||
9. Restart Home Assistant
|
|
||||||
|
|
||||||
### Manual Installation
|
### Manual Installation
|
||||||
1. Download the latest release
|
1. Download the latest release
|
||||||
@@ -94,8 +141,9 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
|||||||
### Via YAML
|
### Via YAML
|
||||||
```yaml
|
```yaml
|
||||||
ha_text_ai:
|
ha_text_ai:
|
||||||
|
api_provider: openai # or anthropic
|
||||||
api_key: !secret ai_api_key
|
api_key: !secret ai_api_key
|
||||||
model: gpt-3.5-turbo # or claude-3-sonnet
|
model: gpt-4o-mini
|
||||||
temperature: 0.7
|
temperature: 0.7
|
||||||
max_tokens: 1000
|
max_tokens: 1000
|
||||||
request_interval: 1.0
|
request_interval: 1.0
|
||||||
@@ -115,6 +163,7 @@ data:
|
|||||||
model: "claude-3-sonnet" # optional
|
model: "claude-3-sonnet" # optional
|
||||||
temperature: 0.5 # optional
|
temperature: 0.5 # optional
|
||||||
max_tokens: 500 # optional
|
max_tokens: 500 # optional
|
||||||
|
context_messages: 10 #optional, number of previous messages to include in context, default: 5
|
||||||
system_prompt: "You are a sleep optimization expert" # optional
|
system_prompt: "You are a sleep optimization expert" # optional
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -140,38 +189,34 @@ service: ha_text_ai.clear_history
|
|||||||
service: ha_text_ai.get_history
|
service: ha_text_ai.get_history
|
||||||
data:
|
data:
|
||||||
limit: 5 # optional
|
limit: 5 # optional
|
||||||
filter_model: "gpt-4" # optional
|
filter_model: "gpt-4o" # optional
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📘 FAQ
|
## 📘 FAQ
|
||||||
|
|
||||||
**Q: Which AI providers are supported?**
|
**Q: Which AI providers are supported?**
|
||||||
|
|
||||||
A: Currently OpenAI (GPT models) and Anthropic (Claude models) are supported, with more providers planned.
|
A: Currently OpenAI (GPT models) and Anthropic (Claude models) are supported, with more providers planned.
|
||||||
|
|
||||||
**Q: How can I reduce API costs?**
|
**Q: How can I reduce API costs?**
|
||||||
|
|
||||||
A: Use GPT-3.5-Turbo or Claude-3-Sonnet for most queries, implement caching, and optimize token usage.
|
A: Use GPT-3.5-Turbo or Claude-3-Sonnet for most queries, implement caching, and optimize token usage.
|
||||||
|
|
||||||
**Q: Can I use custom models?**
|
**Q: Are there limitations on the number of requests?**
|
||||||
|
A: Depends on your API provider's plan. We recommend monitoring usage and implementing request throttling via `request_interval` configuration.
|
||||||
|
|
||||||
|
**Q: Can I use custom models?**
|
||||||
A: Yes, you can configure custom endpoints and use any compatible model by specifying it in the configuration.
|
A: Yes, you can configure custom endpoints and use any compatible model by specifying it in the configuration.
|
||||||
|
|
||||||
**Q: How do I switch between different AI providers?**
|
**Q: How do I switch between different AI providers?**
|
||||||
|
|
||||||
A: Simply change the model parameter in your configuration or service calls to use the desired provider's model.
|
A: Simply change the model parameter in your configuration or service calls to use the desired provider's model.
|
||||||
|
|
||||||
**Q: How can I reduce API costs?**
|
**Q: How can I reduce API costs?**
|
||||||
|
|
||||||
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage.
|
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage.
|
||||||
|
|
||||||
**Q: Is my data secure?**
|
**Q: Is my data secure?**
|
||||||
|
A: Yes, your data is secure. The system operates entirely on your local machine, keeping your data under your control. API keys are stored securely and all external communications use encrypted connections.
|
||||||
|
|
||||||
A: Yes, API keys are stored securely and data is transmitted via encrypted connections.
|
**Q: How do context messages work?**
|
||||||
|
A: Context messages allow the AI to remember and reference previous conversation history. By default, 5 previous messages are included, but you can customize this from 1 to 20 messages to control the conversation depth and token usage.
|
||||||
**Q: Can I use custom models?**
|
|
||||||
|
|
||||||
A: Yes, configure custom endpoints and models via configuration options.
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
"""The HA Text AI integration."""
|
"""The HA Text AI integration."""
|
||||||
import logging
|
from __future__ import annotations
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
import asyncio
|
|
||||||
import voluptuous as vol
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
import logging
|
||||||
from homeassistant.const import CONF_API_KEY
|
import os
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
import shutil
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
from datetime import datetime, timedelta
|
||||||
from homeassistant.helpers import aiohttp_client
|
from typing import Any, Dict
|
||||||
from homeassistant.helpers import config_validation as cv
|
|
||||||
|
import voluptuous as vol
|
||||||
from async_timeout import timeout
|
from async_timeout import timeout
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
from .coordinator import HATextAICoordinator
|
from .coordinator import HATextAICoordinator
|
||||||
|
from .api_client import APIClient
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
@@ -23,75 +27,161 @@ from .const import (
|
|||||||
CONF_MAX_TOKENS,
|
CONF_MAX_TOKENS,
|
||||||
CONF_API_ENDPOINT,
|
CONF_API_ENDPOINT,
|
||||||
CONF_REQUEST_INTERVAL,
|
CONF_REQUEST_INTERVAL,
|
||||||
|
CONF_API_PROVIDER,
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
API_PROVIDER_OPENAI,
|
||||||
|
API_PROVIDER_ANTHROPIC,
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
DEFAULT_TEMPERATURE,
|
DEFAULT_TEMPERATURE,
|
||||||
DEFAULT_MAX_TOKENS,
|
DEFAULT_MAX_TOKENS,
|
||||||
DEFAULT_API_ENDPOINT,
|
DEFAULT_OPENAI_ENDPOINT,
|
||||||
|
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||||
DEFAULT_REQUEST_INTERVAL,
|
DEFAULT_REQUEST_INTERVAL,
|
||||||
API_VERSION,
|
DEFAULT_CONTEXT_MESSAGES,
|
||||||
API_MODELS_PATH,
|
|
||||||
API_CHAT_PATH,
|
|
||||||
API_TIMEOUT,
|
API_TIMEOUT,
|
||||||
API_RETRY_COUNT,
|
SERVICE_ASK_QUESTION,
|
||||||
API_BACKOFF_FACTOR,
|
SERVICE_CLEAR_HISTORY,
|
||||||
LOGGER_NAME,
|
SERVICE_GET_HISTORY,
|
||||||
STATE_ERROR,
|
SERVICE_SET_SYSTEM_PROMPT,
|
||||||
STATE_READY,
|
|
||||||
STATE_PROCESSING,
|
|
||||||
STATE_RATE_LIMITED,
|
|
||||||
STATE_MAINTENANCE,
|
|
||||||
STATE_DISCONNECTED,
|
|
||||||
STATE_RETRYING,
|
|
||||||
STATE_QUEUED,
|
|
||||||
STATE_UPDATING,
|
|
||||||
SUPPORTED_MODELS,
|
|
||||||
EVENT_RESPONSE_RECEIVED,
|
|
||||||
EVENT_ERROR_OCCURRED,
|
|
||||||
EVENT_STATE_CHANGED,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(LOGGER_NAME)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
# Service validation schemas
|
|
||||||
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||||
|
vol.Required("instance"): cv.string,
|
||||||
vol.Required("question"): cv.string,
|
vol.Required("question"): cv.string,
|
||||||
vol.Optional("system_prompt"): cv.string,
|
vol.Optional("system_prompt"): cv.string,
|
||||||
vol.Optional("model"): vol.In(SUPPORTED_MODELS),
|
vol.Optional("model"): cv.string,
|
||||||
vol.Optional("temperature"): vol.All(
|
vol.Optional("temperature"): cv.positive_float,
|
||||||
vol.Coerce(float), vol.Range(min=0, max=2)
|
vol.Optional("max_tokens"): cv.positive_int,
|
||||||
),
|
vol.Optional("context_messages"): cv.positive_int,
|
||||||
vol.Optional("max_tokens"): vol.All(
|
|
||||||
vol.Coerce(int), vol.Range(min=1, max=4096)
|
|
||||||
),
|
|
||||||
vol.Optional("priority"): vol.Boolean,
|
|
||||||
})
|
|
||||||
|
|
||||||
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
|
||||||
vol.Optional("limit", default=10): vol.All(
|
|
||||||
vol.Coerce(int), vol.Range(min=1, max=100)
|
|
||||||
),
|
|
||||||
vol.Optional("filter_model"): vol.In(SUPPORTED_MODELS),
|
|
||||||
vol.Optional("start_date"): cv.datetime,
|
|
||||||
vol.Optional("include_metadata"): vol.Boolean,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||||
|
vol.Required("instance"): cv.string,
|
||||||
vol.Required("prompt"): cv.string,
|
vol.Required("prompt"): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
async def async_check_api(session, endpoint: str, headers: dict, is_anthropic: bool = False) -> bool:
|
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
||||||
|
vol.Required("instance"): cv.string,
|
||||||
|
vol.Optional("limit"): cv.positive_int,
|
||||||
|
vol.Optional("filter_model"): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator:
|
||||||
|
"""Get coordinator by instance name."""
|
||||||
|
if instance.startswith("sensor."):
|
||||||
|
instance = instance.replace("sensor.ha_text_ai_", "", 1)
|
||||||
|
|
||||||
|
for entry_id, coord in hass.data[DOMAIN].items():
|
||||||
|
if isinstance(coord, HATextAICoordinator) and coord.instance_name.lower() == instance.lower():
|
||||||
|
return coord
|
||||||
|
|
||||||
|
raise HomeAssistantError(f"Instance {instance} not found")
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
||||||
|
"""Set up the HA Text AI component."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
|
try:
|
||||||
|
source = os.path.join(os.path.dirname(__file__), 'icons', 'icon.svg')
|
||||||
|
dest_dir = os.path.join(hass.config.path('www'), 'icons')
|
||||||
|
os.makedirs(dest_dir, exist_ok=True)
|
||||||
|
dest = os.path.join(dest_dir, 'icon.svg')
|
||||||
|
if not os.path.exists(dest):
|
||||||
|
shutil.copyfile(source, dest)
|
||||||
|
except Exception as ex:
|
||||||
|
_LOGGER.warning("Failed to copy custom icon: %s", str(ex))
|
||||||
|
|
||||||
|
async def async_ask_question(call: ServiceCall) -> None:
|
||||||
|
"""Handle ask_question service."""
|
||||||
|
try:
|
||||||
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
|
await coordinator.async_ask_question(
|
||||||
|
question=call.data["question"],
|
||||||
|
model=call.data.get("model"),
|
||||||
|
temperature=call.data.get("temperature"),
|
||||||
|
max_tokens=call.data.get("max_tokens"),
|
||||||
|
system_prompt=call.data.get("system_prompt"),
|
||||||
|
context_messages=call.data.get("context_messages"),
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error asking question: %s", str(err))
|
||||||
|
raise HomeAssistantError(f"Failed to process question: {str(err)}")
|
||||||
|
|
||||||
|
async def async_clear_history(call: ServiceCall) -> None:
|
||||||
|
"""Handle clear_history service."""
|
||||||
|
try:
|
||||||
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
|
await coordinator.async_clear_history()
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error clearing history: %s", str(err))
|
||||||
|
raise HomeAssistantError(f"Failed to clear history: {str(err)}")
|
||||||
|
|
||||||
|
async def async_get_history(call: ServiceCall) -> list:
|
||||||
|
"""Handle get_history service."""
|
||||||
|
try:
|
||||||
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
|
return await coordinator.async_get_history(
|
||||||
|
limit=call.data.get("limit"),
|
||||||
|
filter_model=call.data.get("filter_model")
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error getting history: %s", str(err))
|
||||||
|
raise HomeAssistantError(f"Failed to get history: {str(err)}")
|
||||||
|
|
||||||
|
async def async_set_system_prompt(call: ServiceCall) -> None:
|
||||||
|
"""Handle set_system_prompt service."""
|
||||||
|
try:
|
||||||
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
|
await coordinator.async_set_system_prompt(call.data["prompt"])
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error setting system prompt: %s", str(err))
|
||||||
|
raise HomeAssistantError(f"Failed to set system prompt: {str(err)}")
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_ASK_QUESTION,
|
||||||
|
async_ask_question,
|
||||||
|
schema=SERVICE_SCHEMA_ASK_QUESTION
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_CLEAR_HISTORY,
|
||||||
|
async_clear_history,
|
||||||
|
schema=vol.Schema({vol.Required("instance"): cv.string})
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_GET_HISTORY,
|
||||||
|
async_get_history,
|
||||||
|
schema=SERVICE_SCHEMA_GET_HISTORY
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_SYSTEM_PROMPT,
|
||||||
|
async_set_system_prompt,
|
||||||
|
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
|
||||||
"""Check API availability for different providers."""
|
"""Check API availability for different providers."""
|
||||||
try:
|
try:
|
||||||
if is_anthropic:
|
if provider == API_PROVIDER_ANTHROPIC:
|
||||||
check_url = f"{endpoint}/v1/models"
|
check_url = f"{endpoint}/v1/models"
|
||||||
else:
|
else: # OpenAI
|
||||||
check_url = f"{endpoint}/{API_VERSION}/{API_MODELS_PATH}"
|
check_url = f"{endpoint}/models"
|
||||||
|
|
||||||
async with timeout(API_TIMEOUT):
|
async with timeout(API_TIMEOUT):
|
||||||
async with session.get(check_url, headers=headers) as response:
|
async with session.get(check_url, headers=headers) as response:
|
||||||
if response.status == 200:
|
if response.status in [200, 404]:
|
||||||
return True
|
return True
|
||||||
elif response.status == 401:
|
elif response.status == 401:
|
||||||
raise ConfigEntryNotReady("Invalid API key")
|
raise ConfigEntryNotReady("Invalid API key")
|
||||||
@@ -105,32 +195,25 @@ async def async_check_api(session, endpoint: str, headers: dict, is_anthropic: b
|
|||||||
_LOGGER.error("API check error: %s", str(ex))
|
_LOGGER.error("API check error: %s", str(ex))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
|
|
||||||
"""Set up the HA Text AI component."""
|
|
||||||
hass.data.setdefault(DOMAIN, {
|
|
||||||
"coordinators": {},
|
|
||||||
"metrics": {
|
|
||||||
"total_requests": 0,
|
|
||||||
"total_tokens": 0,
|
|
||||||
"errors": {},
|
|
||||||
"model_usage": {},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up HA Text AI from a config entry."""
|
"""Set up HA Text AI from a config entry."""
|
||||||
try:
|
try:
|
||||||
|
if CONF_API_PROVIDER not in entry.data:
|
||||||
|
_LOGGER.error("API provider not specified")
|
||||||
|
raise ConfigEntryNotReady("API provider is required")
|
||||||
|
|
||||||
session = aiohttp_client.async_get_clientsession(hass)
|
session = aiohttp_client.async_get_clientsession(hass)
|
||||||
|
api_provider = entry.data.get(CONF_API_PROVIDER)
|
||||||
# Determine API type based on model
|
|
||||||
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
|
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
|
||||||
is_anthropic = any(model.startswith("claude") for model in SUPPORTED_MODELS)
|
endpoint = entry.data.get(
|
||||||
|
CONF_API_ENDPOINT,
|
||||||
|
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
|
||||||
|
else DEFAULT_ANTHROPIC_ENDPOINT
|
||||||
|
).rstrip('/')
|
||||||
api_key = entry.data[CONF_API_KEY]
|
api_key = entry.data[CONF_API_KEY]
|
||||||
endpoint = entry.data.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT).rstrip('/')
|
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
|
||||||
|
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
|
||||||
|
|
||||||
# Configure headers based on API type
|
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Accept": "application/json"
|
"Accept": "application/json"
|
||||||
@@ -142,234 +225,71 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
else:
|
else:
|
||||||
headers["Authorization"] = f"Bearer {api_key}"
|
headers["Authorization"] = f"Bearer {api_key}"
|
||||||
|
|
||||||
# Check API with retries
|
if not await async_check_api(session, endpoint, headers, api_provider):
|
||||||
for attempt in range(API_RETRY_COUNT):
|
raise ConfigEntryNotReady("API connection failed")
|
||||||
if await async_check_api(session, endpoint, headers, is_anthropic):
|
|
||||||
break
|
|
||||||
if attempt < API_RETRY_COUNT - 1:
|
|
||||||
delay = API_BACKOFF_FACTOR * (2 ** attempt)
|
|
||||||
await asyncio.sleep(delay)
|
|
||||||
else:
|
|
||||||
raise ConfigEntryNotReady("Failed to connect to API")
|
|
||||||
|
|
||||||
# Create and initialize coordinator
|
_LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint)
|
||||||
coordinator = HATextAICoordinator(
|
|
||||||
hass,
|
api_client = APIClient(
|
||||||
api_key=api_key,
|
|
||||||
endpoint=endpoint,
|
|
||||||
model=model,
|
|
||||||
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
|
||||||
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
|
||||||
request_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
|
||||||
session=session,
|
session=session,
|
||||||
is_anthropic=is_anthropic
|
endpoint=endpoint,
|
||||||
|
headers=headers,
|
||||||
|
api_provider=api_provider,
|
||||||
|
model=model,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize coordinator
|
coordinator = HATextAICoordinator(
|
||||||
|
hass=hass,
|
||||||
|
client=api_client,
|
||||||
|
model=model,
|
||||||
|
update_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
||||||
|
instance_name=instance_name,
|
||||||
|
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
||||||
|
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
||||||
|
is_anthropic=is_anthropic,
|
||||||
|
context_messages=entry.data.get(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
DEFAULT_CONTEXT_MESSAGES
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
coordinator.data = coordinator._initial_state.copy()
|
||||||
|
_LOGGER.debug(f"Initial state set for coordinator {instance_name}")
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
# Check coordinator status
|
hass.data.setdefault(DOMAIN, {})
|
||||||
if coordinator.endpoint_status == "auth_error":
|
|
||||||
raise ConfigEntryNotReady("Authentication failed")
|
|
||||||
elif coordinator.endpoint_status == "rate_limited":
|
|
||||||
_LOGGER.warning("API rate limited during setup")
|
|
||||||
elif coordinator.endpoint_status == "maintenance":
|
|
||||||
raise ConfigEntryNotReady("API is in maintenance mode")
|
|
||||||
elif coordinator.endpoint_status == "error":
|
|
||||||
raise ConfigEntryNotReady("API error during setup")
|
|
||||||
elif not coordinator.last_update_success:
|
|
||||||
raise ConfigEntryNotReady("Failed to initialize coordinator")
|
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
# Register event handlers
|
|
||||||
@callback
|
|
||||||
def handle_state_change(event):
|
|
||||||
"""Handle state changes."""
|
|
||||||
if event.data.get("entity_id").startswith(f"{DOMAIN}."):
|
|
||||||
_LOGGER.debug("State changed: %s", event.data)
|
|
||||||
|
|
||||||
hass.bus.async_listen(EVENT_STATE_CHANGED, handle_state_change)
|
|
||||||
|
|
||||||
# Register services
|
|
||||||
async def async_ask_question(call: ServiceCall) -> None:
|
|
||||||
"""Handle the ask_question service call."""
|
|
||||||
question = call.data.get("question", "")
|
|
||||||
if not question:
|
|
||||||
_LOGGER.error("No question provided in service call")
|
|
||||||
return
|
|
||||||
|
|
||||||
request_params = {}
|
|
||||||
for param in ["system_prompt", "model", "temperature", "max_tokens"]:
|
|
||||||
if param in call.data:
|
|
||||||
request_params[param] = call.data[param]
|
|
||||||
|
|
||||||
try:
|
|
||||||
await coordinator.async_ask_question(question, **request_params)
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error asking question: %s", str(err))
|
|
||||||
|
|
||||||
async def async_clear_history(call: ServiceCall) -> None:
|
|
||||||
"""Handle the clear_history service call."""
|
|
||||||
try:
|
|
||||||
coordinator._responses.clear()
|
|
||||||
await coordinator.async_refresh()
|
|
||||||
_LOGGER.info("History cleared successfully")
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error clearing history: %s", str(err))
|
|
||||||
|
|
||||||
async def async_get_history(call: ServiceCall) -> dict:
|
|
||||||
"""Handle the get_history service call."""
|
|
||||||
try:
|
|
||||||
limit = min(int(call.data.get("limit", 10)), 100)
|
|
||||||
filter_model = str(call.data.get("filter_model", ""))
|
|
||||||
start_date = call.data.get("start_date")
|
|
||||||
include_metadata = call.data.get("include_metadata", False)
|
|
||||||
|
|
||||||
responses = coordinator._responses
|
|
||||||
metrics = {
|
|
||||||
"total_requests": coordinator.request_count,
|
|
||||||
"total_tokens": coordinator.tokens_used,
|
|
||||||
"api_version": coordinator.api_version,
|
|
||||||
"endpoint_status": coordinator.endpoint_status,
|
|
||||||
"error_count": coordinator.error_count
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered_responses = responses.copy()
|
|
||||||
|
|
||||||
if filter_model:
|
|
||||||
filtered_responses = {
|
|
||||||
k: v for k, v in filtered_responses.items()
|
|
||||||
if v.get("model") == filter_model
|
|
||||||
}
|
|
||||||
|
|
||||||
if start_date:
|
|
||||||
filtered_responses = {
|
|
||||||
k: v for k, v in filtered_responses.items()
|
|
||||||
if v.get("timestamp") >= start_date
|
|
||||||
}
|
|
||||||
|
|
||||||
if not include_metadata:
|
|
||||||
filtered_responses = {
|
|
||||||
k: {
|
|
||||||
"question": v["question"],
|
|
||||||
"response": v["response"],
|
|
||||||
"timestamp": v["timestamp"]
|
|
||||||
} for k, v in filtered_responses.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
sorted_responses = dict(
|
|
||||||
sorted(
|
|
||||||
filtered_responses.items(),
|
|
||||||
key=lambda x: x[1]["timestamp"],
|
|
||||||
reverse=True
|
|
||||||
)[:limit]
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"metrics": metrics,
|
|
||||||
"responses": sorted_responses
|
|
||||||
}
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error getting history: %s", str(err))
|
|
||||||
return {}
|
|
||||||
|
|
||||||
async def async_set_system_prompt(call: ServiceCall) -> None:
|
|
||||||
"""Handle the set_system_prompt service call."""
|
|
||||||
try:
|
|
||||||
prompt = str(call.data.get("prompt", "")).strip()
|
|
||||||
if prompt:
|
|
||||||
coordinator.system_prompt = prompt
|
|
||||||
_LOGGER.info("System prompt updated successfully")
|
|
||||||
else:
|
|
||||||
_LOGGER.error("Empty prompt provided")
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error setting system prompt: %s", str(err))
|
|
||||||
|
|
||||||
# Register services with validation
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"ask_question",
|
|
||||||
async_ask_question,
|
|
||||||
schema=SERVICE_SCHEMA_ASK_QUESTION
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"clear_history",
|
|
||||||
async_clear_history
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"get_history",
|
|
||||||
async_get_history,
|
|
||||||
schema=SERVICE_SCHEMA_GET_HISTORY
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"set_system_prompt",
|
|
||||||
async_set_system_prompt,
|
|
||||||
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Successfully set up HA Text AI with model: %s",
|
"Successfully set up %s instance '%s' with model %s",
|
||||||
entry.data.get(CONF_MODEL, DEFAULT_MODEL)
|
api_provider,
|
||||||
|
instance_name,
|
||||||
|
model
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
_LOGGER.exception("Setup error: %s", str(ex))
|
_LOGGER.exception("Setup error: %s", str(ex))
|
||||||
raise ConfigEntryNotReady from ex
|
raise ConfigEntryNotReady(f"Setup error: {str(ex)}") from ex
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
try:
|
try:
|
||||||
coordinator = hass.data[DOMAIN].get(entry.entry_id)
|
if entry.entry_id in hass.data[DOMAIN]:
|
||||||
if coordinator:
|
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
# Clear queue and history
|
|
||||||
coordinator._responses.clear()
|
if hasattr(coordinator.client, 'shutdown'):
|
||||||
while not coordinator._question_queue.empty():
|
await coordinator.client.shutdown()
|
||||||
try:
|
|
||||||
coordinator._question_queue.get_nowait()
|
|
||||||
coordinator._question_queue.task_done()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Close connection
|
|
||||||
await coordinator.async_shutdown()
|
await coordinator.async_shutdown()
|
||||||
|
|
||||||
# Remove services
|
|
||||||
for service in ["ask_question", "clear_history", "get_history", "set_system_prompt"]:
|
|
||||||
hass.services.async_remove(DOMAIN, service)
|
|
||||||
|
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
return unload_ok
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
_LOGGER.exception("Error unloading entry: %s", str(ex))
|
_LOGGER.exception("Error unloading entry: %s", str(ex))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Migrate old entry."""
|
|
||||||
_LOGGER.debug("Migrating from version %s", entry.version)
|
|
||||||
|
|
||||||
if entry.version == 1:
|
|
||||||
new = {**entry.data}
|
|
||||||
|
|
||||||
# Migrate settings
|
|
||||||
if CONF_MODEL in new and new[CONF_MODEL] not in SUPPORTED_MODELS:
|
|
||||||
new[CONF_MODEL] = DEFAULT_MODEL
|
|
||||||
|
|
||||||
entry.version = 2
|
|
||||||
hass.config_entries.async_update_entry(entry, data=new)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
"""API Client for HA Text AI."""
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from aiohttp import ClientSession, ClientTimeout
|
||||||
|
from async_timeout import timeout
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from .const import (
|
||||||
|
API_TIMEOUT,
|
||||||
|
API_RETRY_COUNT,
|
||||||
|
API_PROVIDER_ANTHROPIC,
|
||||||
|
MIN_TEMPERATURE,
|
||||||
|
MAX_TEMPERATURE,
|
||||||
|
MIN_MAX_TOKENS,
|
||||||
|
MAX_MAX_TOKENS,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class APIClient:
|
||||||
|
"""API Client for OpenAI and Anthropic."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
session: ClientSession,
|
||||||
|
endpoint: str,
|
||||||
|
headers: Dict[str, str],
|
||||||
|
api_provider: str,
|
||||||
|
model: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize API client."""
|
||||||
|
self.session = session
|
||||||
|
self.endpoint = endpoint
|
||||||
|
self.headers = headers
|
||||||
|
self.api_provider = api_provider
|
||||||
|
self.model = model
|
||||||
|
self.timeout = ClientTimeout(total=API_TIMEOUT)
|
||||||
|
|
||||||
|
def _validate_parameters(
|
||||||
|
self,
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: int,
|
||||||
|
) -> None:
|
||||||
|
"""Validate API parameters."""
|
||||||
|
if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE:
|
||||||
|
raise ValueError(
|
||||||
|
f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}"
|
||||||
|
)
|
||||||
|
if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS:
|
||||||
|
raise ValueError(
|
||||||
|
f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _make_request(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
payload: Dict[str, Any],
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Make API request with retry logic."""
|
||||||
|
for attempt in range(API_RETRY_COUNT):
|
||||||
|
try:
|
||||||
|
async with timeout(API_TIMEOUT):
|
||||||
|
async with self.session.post(
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
headers=self.headers,
|
||||||
|
timeout=self.timeout
|
||||||
|
) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
error_data = await response.json()
|
||||||
|
raise HomeAssistantError(f"API error: {error_data}")
|
||||||
|
return await response.json()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
if attempt == API_RETRY_COUNT - 1:
|
||||||
|
raise HomeAssistantError("API request timed out")
|
||||||
|
await asyncio.sleep(1 * (attempt + 1))
|
||||||
|
except Exception as e:
|
||||||
|
if attempt == API_RETRY_COUNT - 1:
|
||||||
|
raise
|
||||||
|
_LOGGER.warning("API request failed, retrying: %s", str(e))
|
||||||
|
await asyncio.sleep(1 * (attempt + 1))
|
||||||
|
|
||||||
|
async def create(
|
||||||
|
self,
|
||||||
|
model: str,
|
||||||
|
messages: List[Dict[str, str]],
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: int,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Create completion using appropriate API."""
|
||||||
|
try:
|
||||||
|
self._validate_parameters(temperature, max_tokens)
|
||||||
|
|
||||||
|
if self.api_provider == API_PROVIDER_ANTHROPIC:
|
||||||
|
return await self._create_anthropic_completion(
|
||||||
|
model, messages, temperature, max_tokens
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return await self._create_openai_completion(
|
||||||
|
model, messages, temperature, max_tokens
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error("API request failed: %s", str(e))
|
||||||
|
raise HomeAssistantError(f"API request failed: {str(e)}")
|
||||||
|
|
||||||
|
async def _create_openai_completion(
|
||||||
|
self,
|
||||||
|
model: str,
|
||||||
|
messages: List[Dict[str, str]],
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: int,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Create completion using OpenAI API."""
|
||||||
|
url = f"{self.endpoint}/chat/completions"
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": messages,
|
||||||
|
"temperature": temperature,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
}
|
||||||
|
|
||||||
|
data = await self._make_request(url, payload)
|
||||||
|
return {
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"message": {
|
||||||
|
"content": data["choices"][0]["message"]["content"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": data["usage"]["prompt_tokens"],
|
||||||
|
"completion_tokens": data["usage"]["completion_tokens"],
|
||||||
|
"total_tokens": data["usage"]["total_tokens"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _create_anthropic_completion(
|
||||||
|
self,
|
||||||
|
model: str,
|
||||||
|
messages: List[Dict[str, str]],
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: int,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Create completion using Anthropic API."""
|
||||||
|
url = f"{self.endpoint}/v1/messages"
|
||||||
|
|
||||||
|
# Convert messages to Anthropic format
|
||||||
|
system_prompt = next(
|
||||||
|
(msg["content"] for msg in messages if msg["role"] == "system"),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
conversation = [msg for msg in messages if msg["role"] != "system"]
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": conversation,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"temperature": temperature,
|
||||||
|
}
|
||||||
|
if system_prompt:
|
||||||
|
payload["system"] = system_prompt
|
||||||
|
|
||||||
|
data = await self._make_request(url, payload)
|
||||||
|
return {
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"message": {
|
||||||
|
"content": data["content"][0]["text"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": data["usage"]["input_tokens"],
|
||||||
|
"completion_tokens": data["usage"]["output_tokens"],
|
||||||
|
"total_tokens": data["usage"]["input_tokens"] + data["usage"]["output_tokens"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
"""Config flow for HA text AI integration."""
|
"""Config flow for HA text AI integration."""
|
||||||
from typing import Any, Dict, Optional, Tuple
|
import logging
|
||||||
import voluptuous as vol
|
from typing import Any, Dict, Optional
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
from async_timeout import timeout
|
|
||||||
from urllib.parse import urlparse, urljoin
|
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers import selector
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@@ -20,269 +17,231 @@ from .const import (
|
|||||||
CONF_MAX_TOKENS,
|
CONF_MAX_TOKENS,
|
||||||
CONF_API_ENDPOINT,
|
CONF_API_ENDPOINT,
|
||||||
CONF_REQUEST_INTERVAL,
|
CONF_REQUEST_INTERVAL,
|
||||||
|
CONF_API_PROVIDER,
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
API_PROVIDER_OPENAI,
|
||||||
|
API_PROVIDER_ANTHROPIC,
|
||||||
|
API_PROVIDERS,
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
DEFAULT_TEMPERATURE,
|
DEFAULT_TEMPERATURE,
|
||||||
DEFAULT_MAX_TOKENS,
|
DEFAULT_MAX_TOKENS,
|
||||||
DEFAULT_API_ENDPOINT,
|
|
||||||
DEFAULT_REQUEST_INTERVAL,
|
DEFAULT_REQUEST_INTERVAL,
|
||||||
|
DEFAULT_OPENAI_ENDPOINT,
|
||||||
|
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||||
|
DEFAULT_CONTEXT_MESSAGES,
|
||||||
MIN_TEMPERATURE,
|
MIN_TEMPERATURE,
|
||||||
MAX_TEMPERATURE,
|
MAX_TEMPERATURE,
|
||||||
MIN_MAX_TOKENS,
|
MIN_MAX_TOKENS,
|
||||||
MAX_MAX_TOKENS,
|
MAX_MAX_TOKENS,
|
||||||
MIN_REQUEST_INTERVAL,
|
MIN_REQUEST_INTERVAL,
|
||||||
API_VERSION,
|
|
||||||
API_MODELS_PATH,
|
|
||||||
ERROR_INVALID_API_KEY,
|
|
||||||
ERROR_CANNOT_CONNECT,
|
|
||||||
ERROR_UNKNOWN,
|
|
||||||
ERROR_INVALID_MODEL,
|
|
||||||
ERROR_RATE_LIMIT,
|
|
||||||
ERROR_API_ERROR,
|
|
||||||
ERROR_TIMEOUT,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
import logging
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_API_KEY): cv.string,
|
|
||||||
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_TEMPERATURE,
|
|
||||||
default=DEFAULT_TEMPERATURE
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(float),
|
|
||||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
|
||||||
),
|
|
||||||
vol.Optional(
|
|
||||||
CONF_MAX_TOKENS,
|
|
||||||
default=DEFAULT_MAX_TOKENS
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(int),
|
|
||||||
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
|
||||||
),
|
|
||||||
vol.Optional(
|
|
||||||
CONF_API_ENDPOINT,
|
|
||||||
default=DEFAULT_API_ENDPOINT
|
|
||||||
): cv.string,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_REQUEST_INTERVAL,
|
|
||||||
default=DEFAULT_REQUEST_INTERVAL
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(float),
|
|
||||||
vol.Range(min=MIN_REQUEST_INTERVAL)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def validate_api_connection(
|
|
||||||
hass,
|
|
||||||
api_key: str,
|
|
||||||
endpoint: str,
|
|
||||||
model: str,
|
|
||||||
retry_count: int = 3,
|
|
||||||
retry_delay: float = 1.0
|
|
||||||
) -> Tuple[bool, str, list]:
|
|
||||||
"""Validate API connection with retry logic."""
|
|
||||||
session = async_get_clientsession(hass)
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
"Accept": "application/json; charset=utf-8",
|
|
||||||
"Accept-Charset": "utf-8"
|
|
||||||
}
|
|
||||||
|
|
||||||
base_url = endpoint.rstrip('/')
|
|
||||||
if not base_url.endswith(f"/{API_VERSION}"):
|
|
||||||
base_url = f"{base_url}/{API_VERSION}"
|
|
||||||
|
|
||||||
models_url = f"{base_url}/{API_MODELS_PATH}"
|
|
||||||
_LOGGER.debug("Attempting to connect to: %s", models_url)
|
|
||||||
|
|
||||||
for attempt in range(retry_count):
|
|
||||||
try:
|
|
||||||
async with timeout(10):
|
|
||||||
async with session.get(models_url, headers=headers) as response:
|
|
||||||
if response.status == 200:
|
|
||||||
data = await response.json()
|
|
||||||
model_ids = [m["id"] for m in data.get("data", [])]
|
|
||||||
|
|
||||||
_LOGGER.debug("Available models: %s", ", ".join(model_ids))
|
|
||||||
|
|
||||||
if model not in model_ids:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Model %s not found in available models: %s",
|
|
||||||
model,
|
|
||||||
", ".join(model_ids)
|
|
||||||
)
|
|
||||||
return False, ERROR_INVALID_MODEL, model_ids
|
|
||||||
return True, "", model_ids
|
|
||||||
|
|
||||||
elif response.status == 401:
|
|
||||||
_LOGGER.error("Authentication failed")
|
|
||||||
return False, ERROR_INVALID_API_KEY, []
|
|
||||||
|
|
||||||
elif response.status == 429:
|
|
||||||
_LOGGER.error("Rate limit exceeded")
|
|
||||||
return False, ERROR_RATE_LIMIT, []
|
|
||||||
|
|
||||||
else:
|
|
||||||
response_text = await response.text()
|
|
||||||
_LOGGER.error(
|
|
||||||
"API error: %s - %s",
|
|
||||||
response.status,
|
|
||||||
response_text
|
|
||||||
)
|
|
||||||
return False, ERROR_API_ERROR, []
|
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Timeout during API validation (attempt %d/%d)",
|
|
||||||
attempt + 1,
|
|
||||||
retry_count
|
|
||||||
)
|
|
||||||
if attempt == retry_count - 1:
|
|
||||||
return False, ERROR_TIMEOUT, []
|
|
||||||
await asyncio.sleep(retry_delay)
|
|
||||||
|
|
||||||
except aiohttp.ClientError as err:
|
|
||||||
_LOGGER.error("Connection error: %s", str(err))
|
|
||||||
return False, ERROR_CANNOT_CONNECT, []
|
|
||||||
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.exception("Unexpected error during validation: %s", str(err))
|
|
||||||
return False, ERROR_UNKNOWN, []
|
|
||||||
|
|
||||||
return False, ERROR_UNKNOWN, []
|
|
||||||
|
|
||||||
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for HA text AI."""
|
"""Handle a config flow for HA text AI."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
async def async_step_user(
|
def __init__(self) -> None:
|
||||||
self,
|
"""Initialize flow."""
|
||||||
user_input: Optional[Dict[str, Any]] = None
|
self._errors = {}
|
||||||
) -> FlowResult:
|
self._data = {}
|
||||||
|
self._provider = None
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
errors: Dict[str, str] = {}
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
if user_input is not None:
|
step_id="user",
|
||||||
try:
|
data_schema=vol.Schema({
|
||||||
endpoint = user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT)
|
vol.Required(CONF_API_PROVIDER): selector.SelectSelector(
|
||||||
try:
|
selector.SelectSelectorConfig(
|
||||||
result = urlparse(endpoint)
|
options=API_PROVIDERS,
|
||||||
if not all([result.scheme, result.netloc]):
|
translation_key="api_provider"
|
||||||
errors["base"] = "invalid_url_format"
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
|
||||||
errors=errors
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
),
|
||||||
_LOGGER.error("URL parsing error: %s", str(e))
|
})
|
||||||
errors["base"] = "invalid_url_format"
|
)
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
|
||||||
errors=errors
|
|
||||||
)
|
|
||||||
|
|
||||||
validated_input = STEP_USER_DATA_SCHEMA(user_input)
|
self._provider = user_input[CONF_API_PROVIDER]
|
||||||
|
return await self.async_step_provider()
|
||||||
|
|
||||||
is_valid, error_code, available_models = await validate_api_connection(
|
async def async_step_provider(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||||
self.hass,
|
"""Handle provider configuration step."""
|
||||||
validated_input[CONF_API_KEY],
|
if user_input is None:
|
||||||
endpoint,
|
default_endpoint = (
|
||||||
validated_input[CONF_MODEL]
|
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
|
||||||
)
|
else DEFAULT_ANTHROPIC_ENDPOINT
|
||||||
|
)
|
||||||
|
|
||||||
if is_valid:
|
suggested_name = f"HA Text AI {len(self._async_current_entries()) + 1}"
|
||||||
await self.async_set_unique_id(validated_input[CONF_API_KEY])
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_show_form(
|
||||||
title="HA Text AI",
|
step_id="provider",
|
||||||
data=validated_input
|
data_schema=vol.Schema({
|
||||||
)
|
vol.Required(CONF_NAME, default=suggested_name): str,
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=default_endpoint): str,
|
||||||
|
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
default=DEFAULT_CONTEXT_MESSAGES
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
errors=self._errors
|
||||||
|
)
|
||||||
|
|
||||||
errors["base"] = error_code
|
instance_name = user_input[CONF_NAME]
|
||||||
if error_code == ERROR_INVALID_MODEL:
|
await self._async_validate_name(instance_name)
|
||||||
_LOGGER.warning(
|
if self._errors:
|
||||||
"Selected model %s not found in available models: %s",
|
return await self.async_step_provider()
|
||||||
validated_input[CONF_MODEL],
|
|
||||||
", ".join(available_models)
|
|
||||||
)
|
|
||||||
|
|
||||||
except vol.Invalid as err:
|
if not await self._async_validate_api(user_input):
|
||||||
_LOGGER.error("Validation error: %s", str(err))
|
return await self.async_step_provider()
|
||||||
errors["base"] = "invalid_input"
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return await self._create_entry(user_input)
|
||||||
step_id="user",
|
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
async def _async_validate_name(self, name: str) -> bool:
|
||||||
errors=errors,
|
"""Validate that the name is unique."""
|
||||||
description_placeholders={
|
for entry in self._async_current_entries():
|
||||||
"default_model": DEFAULT_MODEL,
|
if entry.data.get(CONF_NAME) == name:
|
||||||
"default_endpoint": DEFAULT_API_ENDPOINT,
|
self._errors["name"] = "name_exists"
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
|
||||||
|
"""Validate API connection."""
|
||||||
|
try:
|
||||||
|
session = async_get_clientsession(self.hass)
|
||||||
|
headers = self._get_api_headers(user_input)
|
||||||
|
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
|
||||||
|
|
||||||
|
check_url = (
|
||||||
|
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
|
||||||
|
else f"{endpoint}/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
async with session.get(check_url, headers=headers) as response:
|
||||||
|
if response.status == 401:
|
||||||
|
self._errors["base"] = "invalid_auth"
|
||||||
|
return False
|
||||||
|
elif response.status not in [200, 404]:
|
||||||
|
self._errors["base"] = "cannot_connect"
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("API validation error: %s", str(err))
|
||||||
|
self._errors["base"] = "cannot_connect"
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
|
||||||
|
"""Get API headers based on provider."""
|
||||||
|
api_key = user_input[CONF_API_KEY]
|
||||||
|
|
||||||
|
if self._provider == API_PROVIDER_ANTHROPIC:
|
||||||
|
return {
|
||||||
|
"x-api-key": api_key,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _create_entry(self, user_input: Dict[str, Any]) -> FlowResult:
|
||||||
|
"""Create the config entry."""
|
||||||
|
instance_name = user_input[CONF_NAME]
|
||||||
|
unique_id = f"{DOMAIN}_{instance_name}_{self._provider}".lower().replace(" ", "_")
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=instance_name,
|
||||||
|
data={
|
||||||
|
CONF_API_PROVIDER: self._provider,
|
||||||
|
CONF_NAME: instance_name,
|
||||||
|
**user_input,
|
||||||
|
"unique_id": unique_id,
|
||||||
|
CONF_CONTEXT_MESSAGES: user_input.get(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
DEFAULT_CONTEXT_MESSAGES)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow:
|
||||||
config_entry: config_entries.ConfigEntry,
|
|
||||||
) -> config_entries.OptionsFlow:
|
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return OptionsFlowHandler(config_entry)
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
"""Handle options flow for HA text AI."""
|
"""Handle options flow."""
|
||||||
|
|
||||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
"""Initialize options flow."""
|
"""Initialize options flow."""
|
||||||
self.config_entry = config_entry
|
self.config_entry = config_entry
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||||
self,
|
"""Manage the options."""
|
||||||
user_input: Optional[Dict[str, Any]] = None
|
|
||||||
) -> FlowResult:
|
|
||||||
"""Handle options flow."""
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return self.async_create_entry(title="", data=user_input)
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
options_schema = vol.Schema({
|
current_data = {**self.config_entry.data, **self.config_entry.options}
|
||||||
vol.Optional(
|
|
||||||
CONF_TEMPERATURE,
|
|
||||||
default=self.config_entry.options.get(
|
|
||||||
CONF_TEMPERATURE, DEFAULT_TEMPERATURE
|
|
||||||
),
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(float),
|
|
||||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
|
||||||
),
|
|
||||||
vol.Optional(
|
|
||||||
CONF_MAX_TOKENS,
|
|
||||||
default=self.config_entry.options.get(
|
|
||||||
CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS
|
|
||||||
),
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(int),
|
|
||||||
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
|
||||||
),
|
|
||||||
vol.Optional(
|
|
||||||
CONF_REQUEST_INTERVAL,
|
|
||||||
default=self.config_entry.options.get(
|
|
||||||
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
|
|
||||||
),
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(float),
|
|
||||||
vol.Range(min=MIN_REQUEST_INTERVAL)
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="init",
|
step_id="init",
|
||||||
data_schema=options_schema,
|
data_schema=vol.Schema({
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MODEL,
|
||||||
|
default=current_data.get(CONF_MODEL, DEFAULT_MODEL)
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_TOKENS,
|
||||||
|
default=current_data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_REQUEST_INTERVAL,
|
||||||
|
default=current_data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
default=current_data.get(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
DEFAULT_CONTEXT_MESSAGES
|
||||||
|
)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
),
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,39 +1,47 @@
|
|||||||
"""Constants for the HA text AI integration."""
|
"""Constants for the HA text AI integration."""
|
||||||
from typing import Final
|
from typing import Final
|
||||||
from homeassistant.const import Platform
|
import voluptuous as vol
|
||||||
|
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
# Domain and platforms
|
# Domain and platforms
|
||||||
DOMAIN: Final = "ha_text_ai"
|
DOMAIN: Final = "ha_text_ai"
|
||||||
PLATFORMS: Final = [Platform.SENSOR]
|
PLATFORMS: Final = [Platform.SENSOR]
|
||||||
|
|
||||||
|
# Provider configuration
|
||||||
|
CONF_API_PROVIDER: Final = "api_provider"
|
||||||
|
API_PROVIDER_OPENAI: Final = "openai"
|
||||||
|
API_PROVIDER_ANTHROPIC: Final = "anthropic"
|
||||||
|
|
||||||
|
API_PROVIDERS: Final = [
|
||||||
|
API_PROVIDER_OPENAI,
|
||||||
|
API_PROVIDER_ANTHROPIC
|
||||||
|
]
|
||||||
|
|
||||||
|
# Default endpoints
|
||||||
|
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
|
||||||
|
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
|
||||||
|
|
||||||
# Configuration constants
|
# Configuration constants
|
||||||
CONF_MODEL: Final = "model"
|
CONF_MODEL: Final = "model"
|
||||||
CONF_TEMPERATURE: Final = "temperature"
|
CONF_TEMPERATURE: Final = "temperature"
|
||||||
CONF_MAX_TOKENS: Final = "max_tokens"
|
CONF_MAX_TOKENS: Final = "max_tokens"
|
||||||
CONF_API_ENDPOINT: Final = "api_endpoint"
|
CONF_API_ENDPOINT: Final = "api_endpoint"
|
||||||
CONF_REQUEST_INTERVAL: Final = "request_interval"
|
CONF_REQUEST_INTERVAL: Final = "request_interval"
|
||||||
|
CONF_INSTANCE: Final = "instance"
|
||||||
# Model constants
|
CONF_MAX_HISTORY_SIZE: Final = "max_history_size"
|
||||||
SUPPORTED_MODELS: Final = [
|
CONF_IS_ANTHROPIC: Final = "is_anthropic"
|
||||||
"gpt-3.5-turbo",
|
CONF_CONTEXT_MESSAGES: Final = "context_messages"
|
||||||
"gpt-3.5-turbo-16k",
|
|
||||||
"gpt-4",
|
|
||||||
"gpt-4-32k",
|
|
||||||
"gpt-4-1106-preview",
|
|
||||||
"claude-3-sonnet",
|
|
||||||
"claude-3-opus"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Default values
|
# Default values
|
||||||
DEFAULT_MODEL: Final = "gpt-3.5-turbo"
|
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
||||||
DEFAULT_TEMPERATURE: Final = 0.1
|
DEFAULT_TEMPERATURE: Final = 0.1
|
||||||
DEFAULT_MAX_TOKENS: Final = 1000
|
DEFAULT_MAX_TOKENS: Final = 1000
|
||||||
DEFAULT_API_ENDPOINT: Final = "https://api.openai.com"
|
|
||||||
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
||||||
DEFAULT_TIMEOUT: Final = 30
|
DEFAULT_TIMEOUT: Final = 30
|
||||||
DEFAULT_QUEUE_SIZE: Final = 100
|
DEFAULT_MAX_HISTORY: Final = 50
|
||||||
DEFAULT_HISTORY_LIMIT: Final = 50
|
DEFAULT_NAME: Final = "HA Text AI"
|
||||||
DEFAULT_RETRY_COUNT: Final = 3
|
DEFAULT_CONTEXT_MESSAGES: Final = 5
|
||||||
|
|
||||||
# Parameter constraints
|
# Parameter constraints
|
||||||
MIN_TEMPERATURE: Final = 0.0
|
MIN_TEMPERATURE: Final = 0.0
|
||||||
@@ -41,25 +49,11 @@ MAX_TEMPERATURE: Final = 2.0
|
|||||||
MIN_MAX_TOKENS: Final = 1
|
MIN_MAX_TOKENS: Final = 1
|
||||||
MAX_MAX_TOKENS: Final = 4096
|
MAX_MAX_TOKENS: Final = 4096
|
||||||
MIN_REQUEST_INTERVAL: Final = 0.1
|
MIN_REQUEST_INTERVAL: Final = 0.1
|
||||||
MIN_TIMEOUT: Final = 5
|
MAX_REQUEST_INTERVAL: Final = 60.0
|
||||||
MAX_TIMEOUT: Final = 120
|
|
||||||
MAX_PROMPT_LENGTH: Final = 1000
|
|
||||||
MAX_HISTORY_LIMIT: Final = 100
|
|
||||||
|
|
||||||
# API constants
|
# API constants
|
||||||
API_VERSION: Final = "v1"
|
|
||||||
API_MODELS_PATH: Final = "models"
|
|
||||||
API_CHAT_PATH: Final = "chat/completions"
|
|
||||||
API_TIMEOUT: Final = 30
|
API_TIMEOUT: Final = 30
|
||||||
API_RETRY_COUNT: Final = 3
|
API_RETRY_COUNT: Final = 3
|
||||||
API_BACKOFF_FACTOR: Final = 1.5
|
|
||||||
API_MAX_RETRIES: Final = 3
|
|
||||||
|
|
||||||
# History constants
|
|
||||||
HISTORY_FILTER_MODEL: Final = "filter_model"
|
|
||||||
HISTORY_FILTER_DATE: Final = "start_date"
|
|
||||||
HISTORY_SORT_ORDER: Final = "sort_order"
|
|
||||||
HISTORY_INCLUDE_METADATA: Final = "include_metadata"
|
|
||||||
|
|
||||||
# Service names
|
# Service names
|
||||||
SERVICE_ASK_QUESTION: Final = "ask_question"
|
SERVICE_ASK_QUESTION: Final = "ask_question"
|
||||||
@@ -67,33 +61,48 @@ SERVICE_CLEAR_HISTORY: Final = "clear_history"
|
|||||||
SERVICE_GET_HISTORY: Final = "get_history"
|
SERVICE_GET_HISTORY: Final = "get_history"
|
||||||
SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt"
|
SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt"
|
||||||
|
|
||||||
# Service descriptions
|
|
||||||
SERVICE_ASK_QUESTION_DESCRIPTION: Final = "Ask a question to the AI model"
|
|
||||||
SERVICE_CLEAR_HISTORY_DESCRIPTION: Final = "Clear conversation history"
|
|
||||||
SERVICE_GET_HISTORY_DESCRIPTION: Final = "Get conversation history"
|
|
||||||
SERVICE_SET_SYSTEM_PROMPT_DESCRIPTION: Final = "Set system prompt for AI model"
|
|
||||||
|
|
||||||
# Attribute keys
|
# Attribute keys
|
||||||
ATTR_QUESTION: Final = "question"
|
ATTR_QUESTION: Final = "question"
|
||||||
ATTR_RESPONSE: Final = "response"
|
ATTR_RESPONSE: Final = "response"
|
||||||
ATTR_LAST_UPDATED: Final = "last_updated"
|
ATTR_INSTANCE: Final = "instance"
|
||||||
ATTR_MODEL: Final = "model"
|
ATTR_MODEL: Final = "model"
|
||||||
ATTR_TEMPERATURE: Final = "temperature"
|
ATTR_TEMPERATURE: Final = "temperature"
|
||||||
ATTR_MAX_TOKENS: Final = "max_tokens"
|
ATTR_MAX_TOKENS: Final = "max_tokens"
|
||||||
ATTR_TOTAL_RESPONSES: Final = "total_responses"
|
|
||||||
ATTR_SYSTEM_PROMPT: Final = "system_prompt"
|
ATTR_SYSTEM_PROMPT: Final = "system_prompt"
|
||||||
ATTR_RESPONSE_TIME: Final = "response_time"
|
|
||||||
ATTR_QUEUE_SIZE: Final = "queue_size"
|
|
||||||
ATTR_API_STATUS: Final = "api_status"
|
ATTR_API_STATUS: Final = "api_status"
|
||||||
ATTR_ERROR_COUNT: Final = "error_count"
|
ATTR_ERROR_COUNT: Final = "error_count"
|
||||||
|
ATTR_CONVERSATION_HISTORY: Final = "conversation_history"
|
||||||
|
|
||||||
|
# Sensor attributes
|
||||||
|
ATTR_TOTAL_RESPONSES: Final = "total_responses"
|
||||||
|
ATTR_TOTAL_ERRORS: Final = "total_errors"
|
||||||
|
ATTR_AVG_RESPONSE_TIME: Final = "average_response_time"
|
||||||
|
ATTR_LAST_REQUEST_TIME: Final = "last_request_time"
|
||||||
ATTR_LAST_ERROR: Final = "last_error"
|
ATTR_LAST_ERROR: Final = "last_error"
|
||||||
|
ATTR_IS_PROCESSING: Final = "is_processing"
|
||||||
|
ATTR_IS_RATE_LIMITED: Final = "is_rate_limited"
|
||||||
|
ATTR_IS_MAINTENANCE: Final = "is_maintenance"
|
||||||
ATTR_API_VERSION: Final = "api_version"
|
ATTR_API_VERSION: Final = "api_version"
|
||||||
ATTR_ENDPOINT_STATUS: Final = "endpoint_status"
|
ATTR_ENDPOINT_STATUS: Final = "endpoint_status"
|
||||||
ATTR_REQUEST_COUNT: Final = "request_count"
|
ATTR_PERFORMANCE_METRICS: Final = "performance_metrics"
|
||||||
ATTR_TOKENS_USED: Final = "tokens_used"
|
ATTR_HISTORY_SIZE: Final = "history_size"
|
||||||
ATTR_RETRY_COUNT: Final = "retry_count"
|
ATTR_UPTIME: Final = "uptime"
|
||||||
ATTR_QUEUE_POSITION: Final = "queue_position"
|
ATTR_API_PROVIDER: Final = "api_provider"
|
||||||
ATTR_ESTIMATED_WAIT: Final = "estimated_wait"
|
ATTR_METRICS: Final = "metrics"
|
||||||
|
ATTR_STATE: Final = "state"
|
||||||
|
ATTR_LAST_RESPONSE: Final = "last_response"
|
||||||
|
ATTR_ERROR: Final = "error"
|
||||||
|
ATTR_TIMESTAMP: Final = "timestamp"
|
||||||
|
|
||||||
|
# Sensor metrics
|
||||||
|
METRIC_TOTAL_TOKENS: Final = "total_tokens"
|
||||||
|
METRIC_PROMPT_TOKENS: Final = "prompt_tokens"
|
||||||
|
METRIC_COMPLETION_TOKENS: Final = "completion_tokens"
|
||||||
|
METRIC_SUCCESSFUL_REQUESTS: Final = "successful_requests"
|
||||||
|
METRIC_FAILED_REQUESTS: Final = "failed_requests"
|
||||||
|
METRIC_AVERAGE_LATENCY: Final = "average_latency"
|
||||||
|
METRIC_MAX_LATENCY: Final = "max_latency"
|
||||||
|
METRIC_MIN_LATENCY: Final = "min_latency"
|
||||||
|
|
||||||
# Error messages
|
# Error messages
|
||||||
ERROR_INVALID_API_KEY: Final = "invalid_api_key"
|
ERROR_INVALID_API_KEY: Final = "invalid_api_key"
|
||||||
@@ -104,81 +113,89 @@ ERROR_RATE_LIMIT: Final = "rate_limit_exceeded"
|
|||||||
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
|
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
|
||||||
ERROR_API_ERROR: Final = "api_error"
|
ERROR_API_ERROR: Final = "api_error"
|
||||||
ERROR_TIMEOUT: Final = "timeout_error"
|
ERROR_TIMEOUT: Final = "timeout_error"
|
||||||
ERROR_QUEUE_FULL: Final = "queue_full"
|
ERROR_INVALID_INSTANCE: Final = "invalid_instance"
|
||||||
ERROR_INVALID_PROMPT: Final = "invalid_prompt"
|
ERROR_NAME_EXISTS: Final = "name_exists"
|
||||||
ERROR_INVALID_PARAMETERS: Final = "invalid_parameters"
|
|
||||||
ERROR_SERVICE_UNAVAILABLE: Final = "service_unavailable"
|
|
||||||
|
|
||||||
# Configuration descriptions
|
|
||||||
CONF_MODEL_DESCRIPTION: Final = "AI model to use for responses"
|
|
||||||
CONF_TEMPERATURE_DESCRIPTION: Final = "Temperature for response generation (0-2)"
|
|
||||||
CONF_MAX_TOKENS_DESCRIPTION: Final = "Maximum tokens in response (1-4096)"
|
|
||||||
CONF_API_ENDPOINT_DESCRIPTION: Final = "API endpoint URL"
|
|
||||||
CONF_REQUEST_INTERVAL_DESCRIPTION: Final = "Minimum time between API requests (seconds)"
|
|
||||||
|
|
||||||
# Entity attributes descriptions
|
|
||||||
ATTR_QUESTION_DESCRIPTION: Final = "Last question asked"
|
|
||||||
ATTR_RESPONSE_DESCRIPTION: Final = "Last response received"
|
|
||||||
ATTR_LAST_UPDATED_DESCRIPTION: Final = "Time of last update"
|
|
||||||
ATTR_MODEL_DESCRIPTION: Final = "Current AI model in use"
|
|
||||||
ATTR_TEMPERATURE_DESCRIPTION: Final = "Current temperature setting"
|
|
||||||
ATTR_MAX_TOKENS_DESCRIPTION: Final = "Current max tokens setting"
|
|
||||||
ATTR_TOTAL_RESPONSES_DESCRIPTION: Final = "Total number of responses"
|
|
||||||
ATTR_SYSTEM_PROMPT_DESCRIPTION: Final = "Current system prompt"
|
|
||||||
ATTR_RESPONSE_TIME_DESCRIPTION: Final = "Time taken for last response"
|
|
||||||
ATTR_QUEUE_SIZE_DESCRIPTION: Final = "Current size of question queue"
|
|
||||||
ATTR_API_STATUS_DESCRIPTION: Final = "Current API connection status"
|
|
||||||
ATTR_ERROR_COUNT_DESCRIPTION: Final = "Total number of errors"
|
|
||||||
ATTR_LAST_ERROR_DESCRIPTION: Final = "Last error message"
|
|
||||||
ATTR_API_VERSION_DESCRIPTION: Final = "Current API version"
|
|
||||||
ATTR_ENDPOINT_STATUS_DESCRIPTION: Final = "Current endpoint status"
|
|
||||||
ATTR_REQUEST_COUNT_DESCRIPTION: Final = "Total number of API requests"
|
|
||||||
ATTR_TOKENS_USED_DESCRIPTION: Final = "Total tokens used"
|
|
||||||
|
|
||||||
# Entity attributes
|
# Entity attributes
|
||||||
ENTITY_NAME: Final = "HA Text AI"
|
|
||||||
ENTITY_ICON: Final = "mdi:robot"
|
ENTITY_ICON: Final = "mdi:robot"
|
||||||
ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
|
ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
|
||||||
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
|
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
|
||||||
ENTITY_ICON_OFFLINE: Final = "mdi:robot-off"
|
|
||||||
ENTITY_ICON_QUEUE: Final = "mdi:robot-confused"
|
|
||||||
|
|
||||||
# Translation keys
|
|
||||||
TRANSLATION_KEY_CONFIG: Final = "config"
|
|
||||||
TRANSLATION_KEY_OPTIONS: Final = "options"
|
|
||||||
TRANSLATION_KEY_ERROR: Final = "error"
|
|
||||||
TRANSLATION_KEY_STATE: Final = "state"
|
|
||||||
TRANSLATION_KEY_SERVICES: Final = "services"
|
|
||||||
|
|
||||||
# State attributes
|
# State attributes
|
||||||
STATE_READY: Final = "ready"
|
STATE_READY: Final = "ready"
|
||||||
STATE_PROCESSING: Final = "processing"
|
STATE_PROCESSING: Final = "processing"
|
||||||
STATE_ERROR: Final = "error"
|
STATE_ERROR: Final = "error"
|
||||||
STATE_DISCONNECTED: Final = "disconnected"
|
|
||||||
STATE_RATE_LIMITED: Final = "rate_limited"
|
|
||||||
STATE_INITIALIZING: Final = "initializing"
|
STATE_INITIALIZING: Final = "initializing"
|
||||||
STATE_MAINTENANCE: Final = "maintenance"
|
STATE_MAINTENANCE: Final = "maintenance"
|
||||||
STATE_RETRYING: Final = "retrying"
|
STATE_RATE_LIMITED: Final = "rate_limited"
|
||||||
STATE_QUEUED: Final = "queued"
|
STATE_DISCONNECTED: Final = "disconnected"
|
||||||
STATE_UPDATING: Final = "updating"
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOGGER_NAME: Final = "custom_components.ha_text_ai"
|
|
||||||
LOG_LEVEL_DEFAULT: Final = "INFO"
|
|
||||||
|
|
||||||
# Queue constants
|
|
||||||
QUEUE_TIMEOUT: Final = 5
|
|
||||||
QUEUE_MAX_SIZE: Final = 100
|
|
||||||
|
|
||||||
# Service schema constants
|
|
||||||
SCHEMA_QUESTION: Final = "question"
|
|
||||||
SCHEMA_MODEL: Final = "model"
|
|
||||||
SCHEMA_TEMPERATURE: Final = "temperature"
|
|
||||||
SCHEMA_MAX_TOKENS: Final = "max_tokens"
|
|
||||||
SCHEMA_PROMPT: Final = "prompt"
|
|
||||||
SCHEMA_LIMIT: Final = "limit"
|
|
||||||
|
|
||||||
# Event names
|
# Event names
|
||||||
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
|
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
|
||||||
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
|
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
|
||||||
EVENT_STATE_CHANGED: Final = f"{DOMAIN}_state_changed"
|
EVENT_STATE_CHANGED: Final = f"{DOMAIN}_state_changed"
|
||||||
|
|
||||||
|
# Service schema constants
|
||||||
|
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||||
|
vol.Required(CONF_INSTANCE): cv.string,
|
||||||
|
vol.Required("question"): cv.string,
|
||||||
|
vol.Optional("system_prompt"): cv.string,
|
||||||
|
vol.Optional("model"): cv.string,
|
||||||
|
vol.Optional("temperature"): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
vol.Optional("max_tokens"): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||||
|
),
|
||||||
|
vol.Optional("context_messages"): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||||
|
vol.Required(CONF_INSTANCE): cv.string,
|
||||||
|
vol.Required("prompt"): cv.string
|
||||||
|
})
|
||||||
|
|
||||||
|
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
||||||
|
vol.Required(CONF_INSTANCE): cv.string,
|
||||||
|
vol.Optional("limit", default=10): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100)
|
||||||
|
),
|
||||||
|
vol.Optional("filter_model"): cv.string
|
||||||
|
})
|
||||||
|
|
||||||
|
# Configuration schema
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Required(CONF_NAME): cv.string,
|
||||||
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
|
vol.Required(CONF_API_PROVIDER): vol.In(API_PROVIDERS),
|
||||||
|
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string,
|
||||||
|
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_API_ENDPOINT): cv.string,
|
||||||
|
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_MAX_HISTORY_SIZE, default=DEFAULT_MAX_HISTORY): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100),
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_CONTEXT_MESSAGES, default=DEFAULT_CONTEXT_MESSAGES): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|||||||
@@ -1,618 +1,377 @@
|
|||||||
"""Data coordinator for HA text AI."""
|
"""The HA Text AI coordinator."""
|
||||||
import asyncio
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Dict, Optional, List
|
from typing import Any, Dict, List, Optional
|
||||||
import time
|
|
||||||
|
|
||||||
from openai import AsyncOpenAI, APIError, AuthenticationError, RateLimitError
|
|
||||||
from anthropic import AsyncAnthropic
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
import async_timeout
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DEFAULT_TIMEOUT,
|
STATE_READY,
|
||||||
MAX_QUEUE_SIZE,
|
STATE_PROCESSING,
|
||||||
MAX_RETRIES,
|
STATE_ERROR,
|
||||||
RETRY_DELAY,
|
STATE_RATE_LIMITED,
|
||||||
SUPPORTED_MODELS,
|
STATE_MAINTENANCE,
|
||||||
|
DEFAULT_MAX_TOKENS,
|
||||||
|
DEFAULT_TEMPERATURE,
|
||||||
|
DEFAULT_MAX_HISTORY,
|
||||||
|
DEFAULT_CONTEXT_MESSAGES,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
class HATextAICoordinator(DataUpdateCoordinator):
|
class HATextAICoordinator(DataUpdateCoordinator):
|
||||||
"""Class to manage fetching data from the API."""
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
api_key: str,
|
client: Any,
|
||||||
endpoint: str,
|
|
||||||
model: str,
|
model: str,
|
||||||
temperature: float,
|
update_interval: int,
|
||||||
max_tokens: int,
|
instance_name: str,
|
||||||
request_interval: float,
|
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||||
session: Optional[Any] = None,
|
temperature: float = DEFAULT_TEMPERATURE,
|
||||||
|
max_history_size: int = DEFAULT_MAX_HISTORY,
|
||||||
|
context_messages: int = DEFAULT_CONTEXT_MESSAGES,
|
||||||
is_anthropic: bool = False,
|
is_anthropic: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize coordinator.
|
"""Initialize coordinator."""
|
||||||
|
self.instance_name = instance_name
|
||||||
|
self.hass = hass
|
||||||
|
self.client = client
|
||||||
|
self.model = model
|
||||||
|
self.temperature = temperature
|
||||||
|
self.max_tokens = max_tokens
|
||||||
|
self.max_history_size = max_history_size
|
||||||
|
self.is_anthropic = is_anthropic
|
||||||
|
|
||||||
|
# Initialize with default state
|
||||||
|
self._initial_state = {
|
||||||
|
"state": STATE_READY,
|
||||||
|
"metrics": {
|
||||||
|
"total_tokens": 0,
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 0,
|
||||||
|
"successful_requests": 0,
|
||||||
|
"failed_requests": 0,
|
||||||
|
"total_errors": 0,
|
||||||
|
"average_latency": 0,
|
||||||
|
"max_latency": 0,
|
||||||
|
"min_latency": float('inf'),
|
||||||
|
},
|
||||||
|
"last_response": {
|
||||||
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
|
"question": "",
|
||||||
|
"response": "",
|
||||||
|
"model": model,
|
||||||
|
"instance": instance_name,
|
||||||
|
"error": None
|
||||||
|
},
|
||||||
|
"is_processing": False,
|
||||||
|
"is_rate_limited": False,
|
||||||
|
"is_maintenance": False,
|
||||||
|
"endpoint_status": "ready",
|
||||||
|
"uptime": 0,
|
||||||
|
"system_prompt": None,
|
||||||
|
"history_size": 0,
|
||||||
|
"conversation_history": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
update_interval_td = timedelta(seconds=update_interval)
|
||||||
|
|
||||||
Args:
|
|
||||||
hass: HomeAssistant instance
|
|
||||||
api_key: API key for the service
|
|
||||||
endpoint: API endpoint URL
|
|
||||||
model: Model name to use
|
|
||||||
temperature: Temperature parameter for generation
|
|
||||||
max_tokens: Maximum tokens to generate
|
|
||||||
request_interval: Interval between requests
|
|
||||||
session: Optional session object
|
|
||||||
is_anthropic: Whether to use Anthropic API
|
|
||||||
"""
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
name=DOMAIN,
|
name=instance_name,
|
||||||
update_interval=timedelta(seconds=request_interval),
|
update_interval=update_interval_td,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._validate_params(api_key, temperature, max_tokens)
|
# Register instance
|
||||||
|
self.hass.data.setdefault(DOMAIN, {})
|
||||||
|
self.hass.data[DOMAIN][instance_name] = self
|
||||||
|
self.context_messages = context_messages
|
||||||
|
|
||||||
self.api_key = api_key
|
self._system_prompt = None
|
||||||
self.endpoint = endpoint
|
self._conversation_history = []
|
||||||
self.model = model
|
self._performance_metrics = self._initial_state["metrics"].copy()
|
||||||
self.temperature = float(temperature)
|
|
||||||
self.max_tokens = int(max_tokens)
|
|
||||||
self._question_queue = asyncio.PriorityQueue(maxsize=MAX_QUEUE_SIZE)
|
|
||||||
self._responses: Dict[str, Any] = {}
|
|
||||||
self.system_prompt: Optional[str] = None
|
|
||||||
self._is_ready = False
|
|
||||||
self._is_processing = False
|
self._is_processing = False
|
||||||
self._is_rate_limited = False
|
self._is_rate_limited = False
|
||||||
self._is_maintenance = False
|
self._is_maintenance = False
|
||||||
self._error_count = 0
|
self.endpoint_status = "ready"
|
||||||
self._MAX_ERRORS = 3
|
self.last_response = self._initial_state["last_response"].copy()
|
||||||
self._request_count = 0
|
self._start_time = dt_util.utcnow()
|
||||||
self._tokens_used = 0
|
|
||||||
self._api_version = "v1"
|
|
||||||
self._endpoint_status = "disconnected"
|
|
||||||
self._performance_metrics: Dict[str, Any] = {
|
|
||||||
"avg_response_time": 0,
|
|
||||||
"total_errors": 0,
|
|
||||||
"success_rate": 100,
|
|
||||||
"requests_per_minute": 0,
|
|
||||||
}
|
|
||||||
self._last_request_time = 0
|
|
||||||
self._is_anthropic = is_anthropic
|
|
||||||
|
|
||||||
if is_anthropic:
|
_LOGGER.info(f"Initialized HA Text AI coordinator with instance: {instance_name}")
|
||||||
self.client = AsyncAnthropic(api_key=self.api_key)
|
|
||||||
else:
|
|
||||||
self.client = AsyncOpenAI(
|
|
||||||
api_key=self.api_key,
|
|
||||||
base_url=self.endpoint,
|
|
||||||
http_client=session,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _validate_params(self, api_key: str, temperature: float, max_tokens: int) -> None:
|
|
||||||
"""Validate initialization parameters."""
|
|
||||||
if not api_key:
|
|
||||||
raise ValueError("API key is required")
|
|
||||||
if not isinstance(temperature, (int, float)) or not 0 <= temperature <= 2:
|
|
||||||
raise ValueError("Temperature must be between 0 and 2")
|
|
||||||
if not isinstance(max_tokens, int) or max_tokens < 1:
|
|
||||||
raise ValueError("Max tokens must be a positive integer")
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> Dict[str, Any]:
|
async def _async_update_data(self) -> Dict[str, Any]:
|
||||||
"""Update data via API."""
|
"""Update data via library."""
|
||||||
if self._question_queue.empty():
|
|
||||||
return self._responses
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with async_timeout.timeout(DEFAULT_TIMEOUT):
|
current_state = self._get_current_state()
|
||||||
self._is_processing = True
|
_LOGGER.debug(f"Updating data for {self.instance_name}, current state: {current_state}")
|
||||||
priority, question_data = await self._question_queue.get()
|
|
||||||
question = question_data["question"]
|
|
||||||
params = question_data["params"]
|
|
||||||
|
|
||||||
try:
|
data = {
|
||||||
response_data = await self._make_api_call(
|
"state": current_state,
|
||||||
question,
|
"metrics": self._performance_metrics,
|
||||||
model=params.get("model"),
|
"last_response": self.last_response,
|
||||||
temperature=params.get("temperature"),
|
"is_processing": self._is_processing,
|
||||||
max_tokens=params.get("max_tokens"),
|
"is_rate_limited": self._is_rate_limited,
|
||||||
system_prompt=params.get("system_prompt")
|
"is_maintenance": self._is_maintenance,
|
||||||
)
|
"endpoint_status": self.endpoint_status,
|
||||||
|
"uptime": (dt_util.utcnow() - self._start_time).total_seconds(),
|
||||||
self._update_metrics(response_data)
|
"system_prompt": self._system_prompt,
|
||||||
self._responses[question] = {
|
"history_size": len(self._conversation_history),
|
||||||
"question": question,
|
"conversation_history": self._conversation_history,
|
||||||
"response": response_data["response"],
|
|
||||||
"error": None,
|
|
||||||
"timestamp": dt_util.utcnow(),
|
|
||||||
"model": response_data["model"],
|
|
||||||
"temperature": params.get("temperature", self.temperature),
|
|
||||||
"max_tokens": params.get("max_tokens", self.max_tokens),
|
|
||||||
"response_time": response_data.get("response_time"),
|
|
||||||
"tokens": response_data.get("tokens", 0),
|
|
||||||
"priority": priority
|
|
||||||
}
|
|
||||||
self._error_count = 0
|
|
||||||
self._is_ready = True
|
|
||||||
self._endpoint_status = "connected"
|
|
||||||
self._request_count += 1
|
|
||||||
self._tokens_used += response_data.get("tokens", 0)
|
|
||||||
self._last_request_time = time.time()
|
|
||||||
|
|
||||||
# Fire event for successful response
|
|
||||||
self.hass.bus.async_fire(f"{DOMAIN}_response_received", {
|
|
||||||
"question": question,
|
|
||||||
"model": response_data["model"],
|
|
||||||
"tokens": response_data.get("tokens", 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
_LOGGER.debug("Response received for question: %s", question)
|
|
||||||
|
|
||||||
except Exception as err:
|
|
||||||
await self._handle_api_error(question, err)
|
|
||||||
finally:
|
|
||||||
self._is_processing = False
|
|
||||||
self._question_queue.task_done()
|
|
||||||
|
|
||||||
return self._responses
|
|
||||||
|
|
||||||
except asyncio.TimeoutError as err:
|
|
||||||
_LOGGER.error("Timeout while processing question")
|
|
||||||
await self._handle_timeout_error()
|
|
||||||
return self._responses
|
|
||||||
|
|
||||||
async def _handle_api_error(self, question: str, error: Exception) -> None:
|
|
||||||
"""Handle API errors with retry logic."""
|
|
||||||
self._error_count += 1
|
|
||||||
self._performance_metrics["total_errors"] += 1
|
|
||||||
error_msg = str(error)
|
|
||||||
|
|
||||||
if isinstance(error, AuthenticationError):
|
|
||||||
error_msg = "Authentication failed - invalid API key"
|
|
||||||
self._is_ready = False
|
|
||||||
self._endpoint_status = "auth_error"
|
|
||||||
elif isinstance(error, RateLimitError):
|
|
||||||
error_msg = "Rate limit exceeded"
|
|
||||||
self._is_rate_limited = True
|
|
||||||
self._endpoint_status = "rate_limited"
|
|
||||||
# Implement exponential backoff
|
|
||||||
await asyncio.sleep(RETRY_DELAY * (2 ** (self._error_count - 1)))
|
|
||||||
elif isinstance(error, APIError):
|
|
||||||
if "maintenance" in str(error).lower():
|
|
||||||
self._is_maintenance = True
|
|
||||||
self._endpoint_status = "maintenance"
|
|
||||||
error_msg = f"API error: {error}"
|
|
||||||
else:
|
|
||||||
self._endpoint_status = "error"
|
|
||||||
|
|
||||||
self._responses[question] = {
|
|
||||||
"question": question,
|
|
||||||
"response": None,
|
|
||||||
"error": error_msg,
|
|
||||||
"timestamp": dt_util.utcnow(),
|
|
||||||
"model": self.model,
|
|
||||||
"temperature": self.temperature,
|
|
||||||
"max_tokens": self.max_tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fire error event
|
|
||||||
self.hass.bus.async_fire(f"{DOMAIN}_error_occurred", {
|
|
||||||
"error_type": type(error).__name__,
|
|
||||||
"error_message": error_msg,
|
|
||||||
"question": question
|
|
||||||
})
|
|
||||||
|
|
||||||
_LOGGER.error("API error (%s): %s", type(error).__name__, error_msg)
|
|
||||||
|
|
||||||
if self._error_count >= self._MAX_ERRORS:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Multiple errors occurred (%d). Coordinator needs attention.",
|
|
||||||
self._error_count
|
|
||||||
)
|
|
||||||
|
|
||||||
def _update_metrics(self, response_data: Dict[str, Any]) -> None:
|
|
||||||
"""Update performance metrics."""
|
|
||||||
response_time = response_data.get("response_time", 0)
|
|
||||||
current_avg = self._performance_metrics["avg_response_time"]
|
|
||||||
self._performance_metrics["avg_response_time"] = (
|
|
||||||
(current_avg * self._request_count + response_time) /
|
|
||||||
(self._request_count + 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
total_requests = self._request_count + 1
|
|
||||||
self._performance_metrics["success_rate"] = (
|
|
||||||
(total_requests - self._performance_metrics["total_errors"]) /
|
|
||||||
total_requests * 100
|
|
||||||
)
|
|
||||||
|
|
||||||
# Calculate requests per minute
|
|
||||||
if self._last_request_time:
|
|
||||||
time_diff = time.time() - self._last_request_time
|
|
||||||
if time_diff > 0:
|
|
||||||
self._performance_metrics["requests_per_minute"] = 60 / time_diff
|
|
||||||
|
|
||||||
async def _make_api_call(
|
|
||||||
self,
|
|
||||||
question: str,
|
|
||||||
model: Optional[str] = None,
|
|
||||||
temperature: Optional[float] = None,
|
|
||||||
max_tokens: Optional[int] = None,
|
|
||||||
system_prompt: Optional[str] = None
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""Make API call to the selected service."""
|
|
||||||
try:
|
|
||||||
start_time = dt_util.utcnow()
|
|
||||||
|
|
||||||
if self._is_anthropic:
|
|
||||||
response = await self._make_anthropic_call(
|
|
||||||
question, model, temperature, max_tokens, system_prompt
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
response = await self._make_openai_call(
|
|
||||||
question, model, temperature, max_tokens, system_prompt
|
|
||||||
)
|
|
||||||
|
|
||||||
response_time = (dt_util.utcnow() - start_time).total_seconds()
|
|
||||||
|
|
||||||
return {
|
|
||||||
**response,
|
|
||||||
"response_time": response_time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Validate data
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("Invalid data format")
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Updated data for {self.instance_name}: {data}")
|
||||||
|
return data
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error in API call: %s", err)
|
_LOGGER.error(f"Error updating data for {self.instance_name}: {err}")
|
||||||
raise
|
return self._initial_state
|
||||||
|
|
||||||
async def _make_anthropic_call(
|
async def async_update_ha_state(self) -> None:
|
||||||
self,
|
"""Update Home Assistant state."""
|
||||||
question: str,
|
try:
|
||||||
model: Optional[str],
|
_LOGGER.debug(f"Requesting state update for {self.instance_name}")
|
||||||
temperature: Optional[float],
|
await self.async_request_refresh()
|
||||||
max_tokens: Optional[int],
|
|
||||||
system_prompt: Optional[str]
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""Make API call to Anthropic."""
|
|
||||||
messages = []
|
|
||||||
if system_prompt:
|
|
||||||
messages.append({"role": "system", "content": system_prompt})
|
|
||||||
messages.append({"role": "user", "content": question})
|
|
||||||
|
|
||||||
completion = await self.client.messages.create(
|
# Force update of all entities
|
||||||
model=model or self.model,
|
for entity_id in self.hass.states.async_entity_ids():
|
||||||
messages=messages,
|
if entity_id.startswith(f"sensor.ha_text_ai_{self.instance_name}"):
|
||||||
temperature=temperature if temperature is not None else self.temperature,
|
self.hass.states.async_set(entity_id, self._get_current_state())
|
||||||
max_tokens=max_tokens if max_tokens is not None else self.max_tokens,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
except Exception as err:
|
||||||
"response": completion.content[0].text,
|
_LOGGER.error(f"Error updating HA state for {self.instance_name}: {err}")
|
||||||
"model": completion.model,
|
|
||||||
"tokens": completion.usage.total_tokens if hasattr(completion, 'usage') else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
async def _make_openai_call(
|
def _get_current_state(self) -> str:
|
||||||
self,
|
"""Get current state based on internal flags."""
|
||||||
question: str,
|
if self._is_processing:
|
||||||
model: Optional[str],
|
return STATE_PROCESSING
|
||||||
temperature: Optional[float],
|
elif self._is_rate_limited:
|
||||||
max_tokens: Optional[int],
|
return STATE_RATE_LIMITED
|
||||||
system_prompt: Optional[str]
|
elif self._is_maintenance:
|
||||||
) -> Dict[str, Any]:
|
return STATE_MAINTENANCE
|
||||||
"""Make API call to OpenAI."""
|
elif self.last_response.get("error"):
|
||||||
messages = []
|
return STATE_ERROR
|
||||||
if system_prompt:
|
return STATE_READY
|
||||||
messages.append({"role": "system", "content": system_prompt})
|
|
||||||
messages.append({"role": "user", "content": question})
|
|
||||||
|
|
||||||
completion = await self.client.chat.completions.create(
|
def _calculate_context_tokens(self, messages: List[Dict[str, str]]) -> int:
|
||||||
model=model or self.model,
|
try:
|
||||||
messages=messages,
|
if self.is_anthropic and hasattr(self.client, 'count_tokens'):
|
||||||
temperature=temperature if temperature is not None else self.temperature,
|
return sum(self.client.count_tokens(msg['content']) for msg in messages)
|
||||||
max_tokens=max_tokens if max_tokens is not None else self.max_tokens,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return sum(len(msg['content']) // 4 for msg in messages)
|
||||||
"response": completion.choices[0].message.content,
|
except Exception as e:
|
||||||
"model": completion.model,
|
_LOGGER.warning(f"Error calculating context tokens: {e}")
|
||||||
"tokens": completion.usage.total_tokens if hasattr(completion, 'usage') else 0
|
return 0
|
||||||
}
|
|
||||||
|
|
||||||
async def async_ask_question(
|
async def async_ask_question(
|
||||||
self,
|
self,
|
||||||
question: str,
|
question: str,
|
||||||
system_prompt: Optional[str] = None,
|
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
temperature: Optional[float] = None,
|
temperature: Optional[float] = None,
|
||||||
max_tokens: Optional[int] = None,
|
max_tokens: Optional[int] = None,
|
||||||
priority: bool = False
|
system_prompt: Optional[str] = None,
|
||||||
) -> None:
|
context_messages: Optional[int] = None,
|
||||||
"""Add question to queue with priority support."""
|
) -> dict:
|
||||||
if not self._is_ready and self._error_count >= self._MAX_ERRORS:
|
"""Process a question with optional parameters."""
|
||||||
_LOGGER.warning("Coordinator is not ready due to previous errors")
|
return await self.async_process_question(
|
||||||
return
|
question, model, temperature, max_tokens, system_prompt, context_messages
|
||||||
|
)
|
||||||
|
|
||||||
question_data = {
|
async def async_process_question(
|
||||||
"question": question,
|
self,
|
||||||
"params": {
|
question: str,
|
||||||
"system_prompt": system_prompt,
|
model: Optional[str] = None,
|
||||||
"model": model,
|
temperature: Optional[float] = None,
|
||||||
"temperature": temperature,
|
max_tokens: Optional[int] = None,
|
||||||
"max_tokens": max_tokens
|
system_prompt: Optional[str] = None,
|
||||||
|
context_messages: Optional[int] = None,
|
||||||
|
) -> dict:
|
||||||
|
temp_context_messages = context_messages or self.context_messages
|
||||||
|
|
||||||
|
if not question:
|
||||||
|
raise ValueError("Question cannot be empty")
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Processing question for instance {self.instance_name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._is_processing = True
|
||||||
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
|
temp_model = model or self.model
|
||||||
|
temp_temperature = temperature or self.temperature
|
||||||
|
temp_max_tokens = max_tokens or self.max_tokens
|
||||||
|
temp_system_prompt = system_prompt or self._system_prompt
|
||||||
|
|
||||||
|
start_time = dt_util.utcnow()
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
if temp_system_prompt:
|
||||||
|
if self.is_anthropic:
|
||||||
|
system_content = f"\n\nHuman: {temp_system_prompt}\n\nAssistant: I understand and will follow these instructions."
|
||||||
|
messages.append({"role": "user", "content": system_content})
|
||||||
|
else:
|
||||||
|
messages.append({"role": "system", "content": temp_system_prompt})
|
||||||
|
|
||||||
|
# Add conversation history
|
||||||
|
context_history = self._conversation_history[-temp_context_messages:]
|
||||||
|
for entry in context_history:
|
||||||
|
messages.append({"role": "user", "content": entry["question"]})
|
||||||
|
messages.append({"role": "assistant", "content": entry["response"]})
|
||||||
|
|
||||||
|
messages.append({"role": "user", "content": question})
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
"model": temp_model,
|
||||||
|
"temperature": temp_temperature,
|
||||||
|
"max_tokens": temp_max_tokens,
|
||||||
|
"messages": messages,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await self.async_process_message(question, **kwargs)
|
||||||
|
|
||||||
|
# Update metrics
|
||||||
|
end_time = dt_util.utcnow()
|
||||||
|
latency = (end_time - start_time).total_seconds()
|
||||||
|
self._update_metrics(latency, response)
|
||||||
|
|
||||||
|
# Update history
|
||||||
|
self._update_history(question, response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
self._handle_error(err)
|
||||||
|
raise HomeAssistantError(f"Failed to process question: {err}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self._is_processing = False
|
||||||
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
|
async def async_process_message(self, question: str, **kwargs) -> dict:
|
||||||
|
"""Process message using the AI client."""
|
||||||
|
try:
|
||||||
|
if self.is_anthropic:
|
||||||
|
response = await self._process_anthropic_message(question, **kwargs)
|
||||||
|
else:
|
||||||
|
response = await self._process_openai_message(question, **kwargs)
|
||||||
|
|
||||||
|
self.last_response = {
|
||||||
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
|
"question": question,
|
||||||
|
"response": response["content"],
|
||||||
|
"model": kwargs.get("model", self.model),
|
||||||
|
"instance": self.instance_name,
|
||||||
|
"error": None
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
self._handle_error(err)
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
|
||||||
|
"""Process message using Anthropic API."""
|
||||||
|
response = await self.client.messages.create(
|
||||||
|
model=kwargs["model"],
|
||||||
|
max_tokens=kwargs["max_tokens"],
|
||||||
|
messages=kwargs["messages"],
|
||||||
|
temperature=kwargs["temperature"],
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"content": response.content[0].text,
|
||||||
|
"tokens": {
|
||||||
|
"prompt": response.usage.input_tokens,
|
||||||
|
"completion": response.usage.output_tokens,
|
||||||
|
"total": response.usage.input_tokens + response.usage.output_tokens
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Priority: 0 for high priority, 1 for normal
|
async def _process_openai_message(self, question: str, **kwargs) -> dict:
|
||||||
priority_level = 0 if priority else 1
|
"""Process message using OpenAI API."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._question_queue.put((priority_level, question_data))
|
response = await self.client.create(
|
||||||
await self.async_refresh()
|
model=kwargs["model"],
|
||||||
except asyncio.QueueFull:
|
messages=kwargs["messages"],
|
||||||
_LOGGER.error("Question queue is full. Try again later.")
|
temperature=kwargs["temperature"],
|
||||||
raise RuntimeError("Queue is full")
|
max_tokens=kwargs["max_tokens"],
|
||||||
|
|
||||||
async def async_shutdown(self) -> None:
|
|
||||||
"""Shutdown the coordinator."""
|
|
||||||
try:
|
|
||||||
while not self._question_queue.empty():
|
|
||||||
try:
|
|
||||||
self._question_queue.get_nowait()
|
|
||||||
self._question_queue.task_done()
|
|
||||||
except asyncio.QueueEmpty:
|
|
||||||
break
|
|
||||||
|
|
||||||
if hasattr(self.client, 'close'):
|
|
||||||
await self.client.close()
|
|
||||||
|
|
||||||
self._is_ready = False
|
|
||||||
self._endpoint_status = "disconnected"
|
|
||||||
|
|
||||||
# Final metrics update
|
|
||||||
self._update_final_metrics()
|
|
||||||
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error during shutdown: %s", err)
|
|
||||||
|
|
||||||
def _update_final_metrics(self) -> None:
|
|
||||||
"""Update final metrics before shutdown."""
|
|
||||||
if self._request_count > 0:
|
|
||||||
self._performance_metrics["final_success_rate"] = (
|
|
||||||
(self._request_count - self._performance_metrics["total_errors"]) /
|
|
||||||
self._request_count * 100
|
|
||||||
)
|
|
||||||
self._performance_metrics["total_requests"] = self._request_count
|
|
||||||
self._performance_metrics["total_tokens"] = self._tokens_used
|
|
||||||
|
|
||||||
@property
|
|
||||||
def performance_metrics(self) -> Dict[str, Any]:
|
|
||||||
"""Return current performance metrics."""
|
|
||||||
return self._performance_metrics
|
|
||||||
|
|
||||||
@property
|
|
||||||
def queue_size(self) -> int:
|
|
||||||
"""Return current queue size."""
|
|
||||||
return self._question_queue.qsize()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_queue_full(self) -> bool:
|
|
||||||
"""Return whether queue is full."""
|
|
||||||
return self._question_queue.full()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_ready(self) -> bool:
|
|
||||||
"""Return if coordinator is ready."""
|
|
||||||
return self._is_ready and self._error_count < self._MAX_ERRORS
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_processing(self) -> bool:
|
|
||||||
"""Return if coordinator is processing."""
|
|
||||||
return self._is_processing
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_rate_limited(self) -> bool:
|
|
||||||
"""Return if coordinator is rate limited."""
|
|
||||||
return self._is_rate_limited
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_maintenance(self) -> bool:
|
|
||||||
"""Return if API is in maintenance."""
|
|
||||||
return self._is_maintenance
|
|
||||||
|
|
||||||
@property
|
|
||||||
def error_count(self) -> int:
|
|
||||||
"""Return current error count."""
|
|
||||||
return self._error_count
|
|
||||||
|
|
||||||
@property
|
|
||||||
def request_count(self) -> int:
|
|
||||||
"""Return total request count."""
|
|
||||||
return self._request_count
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tokens_used(self) -> int:
|
|
||||||
"""Return total tokens used."""
|
|
||||||
return self._tokens_used
|
|
||||||
|
|
||||||
@property
|
|
||||||
def api_version(self) -> str:
|
|
||||||
"""Return API version."""
|
|
||||||
return self._api_version
|
|
||||||
|
|
||||||
@property
|
|
||||||
def endpoint_status(self) -> str:
|
|
||||||
"""Return endpoint status."""
|
|
||||||
return self._endpoint_status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def responses(self) -> Dict[str, Any]:
|
|
||||||
"""Return all responses."""
|
|
||||||
return self._responses
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_response(self) -> Optional[Dict[str, Any]]:
|
|
||||||
"""Return the last response."""
|
|
||||||
if not self._responses:
|
|
||||||
return None
|
|
||||||
return next(iter(self._responses.values()))
|
|
||||||
|
|
||||||
def reset_error_count(self) -> None:
|
|
||||||
"""Reset error counter."""
|
|
||||||
self._error_count = 0
|
|
||||||
self._is_rate_limited = False
|
|
||||||
self._is_maintenance = False
|
|
||||||
if not self._is_ready:
|
|
||||||
self._is_ready = True
|
|
||||||
self._endpoint_status = "connected"
|
|
||||||
|
|
||||||
async def clear_queue(self) -> None:
|
|
||||||
"""Clear the question queue."""
|
|
||||||
try:
|
|
||||||
while not self._question_queue.empty():
|
|
||||||
try:
|
|
||||||
self._question_queue.get_nowait()
|
|
||||||
self._question_queue.task_done()
|
|
||||||
except asyncio.QueueEmpty:
|
|
||||||
break
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error clearing queue: %s", err)
|
|
||||||
|
|
||||||
async def clear_history(self) -> None:
|
|
||||||
"""Clear response history."""
|
|
||||||
self._responses.clear()
|
|
||||||
await self.async_refresh()
|
|
||||||
|
|
||||||
def get_response(self, question: str) -> Optional[Dict[str, Any]]:
|
|
||||||
"""Get specific response by question."""
|
|
||||||
return self._responses.get(question)
|
|
||||||
|
|
||||||
def get_recent_responses(self, limit: int = 10) -> List[Dict[str, Any]]:
|
|
||||||
"""Get most recent responses."""
|
|
||||||
return list(sorted(
|
|
||||||
self._responses.values(),
|
|
||||||
key=lambda x: x["timestamp"],
|
|
||||||
reverse=True
|
|
||||||
))[:limit]
|
|
||||||
|
|
||||||
async def retry_failed_requests(self) -> None:
|
|
||||||
"""Retry failed requests."""
|
|
||||||
failed_requests = [
|
|
||||||
(q, r) for q, r in self._responses.items()
|
|
||||||
if r.get("error") is not None
|
|
||||||
]
|
|
||||||
|
|
||||||
for question, response in failed_requests:
|
|
||||||
await self.async_ask_question(
|
|
||||||
question,
|
|
||||||
system_prompt=response.get("system_prompt"),
|
|
||||||
model=response.get("model"),
|
|
||||||
temperature=response.get("temperature"),
|
|
||||||
max_tokens=response.get("max_tokens"),
|
|
||||||
priority=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_system_prompt(self, new_prompt: str) -> None:
|
return {
|
||||||
"""Update system prompt."""
|
"content": response["choices"][0]["message"]["content"],
|
||||||
self.system_prompt = new_prompt
|
"tokens": {
|
||||||
_LOGGER.info("System prompt updated")
|
"prompt": response["usage"]["prompt_tokens"],
|
||||||
|
"completion": response["usage"]["completion_tokens"],
|
||||||
|
"total": response["usage"]["total_tokens"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Error in OpenAI API call: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
async def health_check(self) -> Dict[str, Any]:
|
def _update_metrics(self, latency: float, response: dict) -> None:
|
||||||
"""Perform health check."""
|
"""Update performance metrics."""
|
||||||
health_status = {
|
metrics = self._performance_metrics
|
||||||
"is_ready": self.is_ready,
|
tokens = response.get("tokens", {})
|
||||||
"is_processing": self.is_processing,
|
|
||||||
"is_rate_limited": self.is_rate_limited,
|
|
||||||
"is_maintenance": self.is_maintenance,
|
|
||||||
"error_count": self.error_count,
|
|
||||||
"endpoint_status": self.endpoint_status,
|
|
||||||
"queue_size": self.queue_size,
|
|
||||||
"request_count": self.request_count,
|
|
||||||
"tokens_used": self.tokens_used,
|
|
||||||
"performance_metrics": self.performance_metrics
|
|
||||||
}
|
|
||||||
|
|
||||||
return health_status
|
metrics["total_tokens"] += tokens.get("total", 0)
|
||||||
|
metrics["prompt_tokens"] += tokens.get("prompt", 0)
|
||||||
|
metrics["completion_tokens"] += tokens.get("completion", 0)
|
||||||
|
metrics["successful_requests"] += 1
|
||||||
|
|
||||||
async def _handle_timeout_error(self) -> None:
|
metrics["average_latency"] = (
|
||||||
"""Handle timeout errors."""
|
(metrics["average_latency"] * (metrics["successful_requests"] - 1) + latency)
|
||||||
self._error_count += 1
|
/ metrics["successful_requests"]
|
||||||
self._endpoint_status = "timeout"
|
)
|
||||||
self._performance_metrics["total_errors"] += 1
|
metrics["max_latency"] = max(metrics["max_latency"], latency)
|
||||||
|
metrics["min_latency"] = min(metrics["min_latency"], latency)
|
||||||
|
|
||||||
if not self._question_queue.empty():
|
def _update_history(self, question: str, response: dict) -> None:
|
||||||
await self.clear_queue()
|
"""Update conversation history."""
|
||||||
|
self._conversation_history.append({
|
||||||
# Fire timeout event
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
self.hass.bus.async_fire(f"{DOMAIN}_timeout_error", {
|
"question": question,
|
||||||
"error_count": self._error_count,
|
"response": response["content"]
|
||||||
"endpoint_status": self._endpoint_status
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def export_metrics(self) -> Dict[str, Any]:
|
while len(self._conversation_history) > self.max_history_size:
|
||||||
"""Export all metrics and statistics."""
|
self._conversation_history.pop(0)
|
||||||
return {
|
|
||||||
"performance": self._performance_metrics,
|
def _handle_error(self, error: Exception) -> None:
|
||||||
"requests": {
|
"""Handle error and update metrics."""
|
||||||
"total": self._request_count,
|
self._performance_metrics["total_errors"] += 1
|
||||||
"successful": self._request_count - self._performance_metrics["total_errors"],
|
self._performance_metrics["failed_requests"] += 1
|
||||||
"failed": self._performance_metrics["total_errors"]
|
|
||||||
},
|
self.last_response = {
|
||||||
"tokens": {
|
"timestamp": dt_util.utcnow().isoformat(),
|
||||||
"total_used": self._tokens_used,
|
"question": "",
|
||||||
"average_per_request": self._tokens_used / self._request_count if self._request_count > 0 else 0
|
"response": "",
|
||||||
},
|
"model": self.model,
|
||||||
"status": {
|
"instance": self.instance_name,
|
||||||
"is_ready": self.is_ready,
|
"error": str(error)
|
||||||
"endpoint_status": self._endpoint_status,
|
|
||||||
"error_count": self._error_count
|
|
||||||
},
|
|
||||||
"queue": {
|
|
||||||
"size": self.queue_size,
|
|
||||||
"is_full": self.is_queue_full
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async def validate_model(self, model: str) -> bool:
|
async def async_clear_history(self) -> None:
|
||||||
"""Validate if model is supported."""
|
"""Clear conversation history."""
|
||||||
return model in SUPPORTED_MODELS
|
self._conversation_history = []
|
||||||
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
async def estimate_tokens(self, text: str) -> int:
|
async def async_get_history(self) -> List[Dict[str, str]]:
|
||||||
"""Estimate token count for text."""
|
"""Get conversation history."""
|
||||||
# Простая оценка: примерно 4 символа на токен
|
return self._conversation_history
|
||||||
return len(text) // 4
|
|
||||||
|
|
||||||
def get_rate_limit_info(self) -> Dict[str, Any]:
|
async def async_set_system_prompt(self, prompt: str) -> None:
|
||||||
"""Get rate limit information."""
|
"""Set system prompt."""
|
||||||
return {
|
self._system_prompt = prompt
|
||||||
"is_rate_limited": self._is_rate_limited,
|
await self.async_update_ha_state()
|
||||||
"retry_after": RETRY_DELAY * (2 ** (self._error_count - 1)) if self._is_rate_limited else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
async def optimize_queue(self) -> None:
|
|
||||||
"""Optimize queue by removing duplicate requests."""
|
|
||||||
if self._question_queue.empty():
|
|
||||||
return
|
|
||||||
|
|
||||||
seen_questions = set()
|
|
||||||
optimized_queue = asyncio.PriorityQueue(maxsize=MAX_QUEUE_SIZE)
|
|
||||||
|
|
||||||
while not self._question_queue.empty():
|
|
||||||
try:
|
|
||||||
priority, question_data = self._question_queue.get_nowait()
|
|
||||||
question = question_data["question"]
|
|
||||||
|
|
||||||
if question not in seen_questions:
|
|
||||||
seen_questions.add(question)
|
|
||||||
await optimized_queue.put((priority, question_data))
|
|
||||||
|
|
||||||
self._question_queue.task_done()
|
|
||||||
except asyncio.QueueEmpty:
|
|
||||||
break
|
|
||||||
|
|
||||||
self._question_queue = optimized_queue
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 678 KiB |
@@ -1,42 +1,28 @@
|
|||||||
{
|
{
|
||||||
"domain": "ha_text_ai",
|
"domain": "ha_text_ai",
|
||||||
"name": "HA Text AI",
|
"name": "HA Text AI",
|
||||||
|
"after_dependencies": ["http"],
|
||||||
|
"bluetooth": [],
|
||||||
"codeowners": ["@smkrv"],
|
"codeowners": ["@smkrv"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"documentation": "https://github.com/smkrv/ha-text-ai",
|
"documentation": "https://github.com/smkrv/ha-text-ai",
|
||||||
"homepage": "https://github.com/smkrv/ha-text-ai",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
|
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
|
||||||
|
"loggers": ["custom_components.ha_text_ai"],
|
||||||
|
"mqtt": [],
|
||||||
|
"quality_scale": "silver",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"openai>=1.12.0",
|
"openai>=1.12.0",
|
||||||
"anthropic>=0.8.0",
|
"anthropic>=0.8.0",
|
||||||
"aiohttp>=3.8.0",
|
"aiohttp>=3.8.0",
|
||||||
"async-timeout>=4.0.0"
|
"async-timeout>=4.0.0",
|
||||||
|
"certifi>=2024.2.2"
|
||||||
],
|
],
|
||||||
"version": "2.0.0",
|
"single_config_entry": false,
|
||||||
"homeassistant": "2024.1.0",
|
|
||||||
"quality_scale": "silver",
|
|
||||||
"integration_type": "service",
|
|
||||||
"translations": [
|
|
||||||
"en"
|
|
||||||
],
|
|
||||||
"zeroconf": [],
|
|
||||||
"ssdp": [],
|
"ssdp": [],
|
||||||
"usb": [],
|
"usb": [],
|
||||||
"bluetooth": [],
|
"version": "2.0.0",
|
||||||
"mqtt": [],
|
"zeroconf": []
|
||||||
"loggers": ["custom_components.ha_text_ai"],
|
|
||||||
"after_dependencies": ["http"],
|
|
||||||
"title": "Text AI Assistant",
|
|
||||||
"description": "AI-powered text assistant supporting multiple models including GPT and Claude",
|
|
||||||
"render_readme": true,
|
|
||||||
"tags": [
|
|
||||||
"ai",
|
|
||||||
"automation",
|
|
||||||
"chat",
|
|
||||||
"assistant",
|
|
||||||
"openai",
|
|
||||||
"claude"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,65 @@
|
|||||||
"""Sensor platform for HA text AI."""
|
"""Sensor platform for HA Text AI."""
|
||||||
from datetime import datetime
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
import math
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorStateClass,
|
SensorEntityDescription,
|
||||||
SensorDeviceClass,
|
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ATTR_QUESTION,
|
CONF_MODEL,
|
||||||
ATTR_RESPONSE,
|
CONF_API_PROVIDER,
|
||||||
ATTR_LAST_UPDATED,
|
|
||||||
ATTR_MODEL,
|
|
||||||
ATTR_TEMPERATURE,
|
|
||||||
ATTR_MAX_TOKENS,
|
|
||||||
ATTR_TOTAL_RESPONSES,
|
ATTR_TOTAL_RESPONSES,
|
||||||
ATTR_SYSTEM_PROMPT,
|
ATTR_TOTAL_ERRORS,
|
||||||
ATTR_QUEUE_SIZE,
|
ATTR_AVG_RESPONSE_TIME,
|
||||||
ATTR_API_STATUS,
|
ATTR_LAST_REQUEST_TIME,
|
||||||
ATTR_ERROR_COUNT,
|
|
||||||
ATTR_LAST_ERROR,
|
ATTR_LAST_ERROR,
|
||||||
ATTR_RESPONSE_TIME,
|
ATTR_IS_PROCESSING,
|
||||||
|
ATTR_IS_RATE_LIMITED,
|
||||||
|
ATTR_IS_MAINTENANCE,
|
||||||
ATTR_API_VERSION,
|
ATTR_API_VERSION,
|
||||||
ATTR_ENDPOINT_STATUS,
|
ATTR_ENDPOINT_STATUS,
|
||||||
ATTR_REQUEST_COUNT,
|
ATTR_PERFORMANCE_METRICS,
|
||||||
ATTR_TOKENS_USED,
|
ATTR_HISTORY_SIZE,
|
||||||
ENTITY_ICON,
|
ATTR_UPTIME,
|
||||||
ENTITY_ICON_ERROR,
|
ATTR_API_PROVIDER,
|
||||||
ENTITY_ICON_PROCESSING,
|
ATTR_MODEL,
|
||||||
|
ATTR_SYSTEM_PROMPT,
|
||||||
|
ATTR_API_STATUS,
|
||||||
|
ATTR_RESPONSE,
|
||||||
|
ATTR_QUESTION,
|
||||||
|
ATTR_CONVERSATION_HISTORY,
|
||||||
|
METRIC_TOTAL_TOKENS,
|
||||||
|
METRIC_PROMPT_TOKENS,
|
||||||
|
METRIC_COMPLETION_TOKENS,
|
||||||
|
METRIC_SUCCESSFUL_REQUESTS,
|
||||||
|
METRIC_FAILED_REQUESTS,
|
||||||
|
METRIC_AVERAGE_LATENCY,
|
||||||
|
METRIC_MAX_LATENCY,
|
||||||
|
METRIC_MIN_LATENCY,
|
||||||
STATE_READY,
|
STATE_READY,
|
||||||
STATE_PROCESSING,
|
STATE_PROCESSING,
|
||||||
STATE_ERROR,
|
STATE_ERROR,
|
||||||
STATE_DISCONNECTED,
|
|
||||||
STATE_RATE_LIMITED,
|
|
||||||
STATE_INITIALIZING,
|
STATE_INITIALIZING,
|
||||||
STATE_MAINTENANCE,
|
STATE_MAINTENANCE,
|
||||||
|
STATE_RATE_LIMITED,
|
||||||
|
STATE_DISCONNECTED,
|
||||||
|
ENTITY_ICON,
|
||||||
|
ENTITY_ICON_ERROR,
|
||||||
|
ENTITY_ICON_PROCESSING,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .coordinator import HATextAICoordinator
|
from .coordinator import HATextAICoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -54,17 +69,19 @@ async def async_setup_entry(
|
|||||||
entry: ConfigEntry,
|
entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the HA text AI sensor."""
|
"""Set up the HA Text AI sensor."""
|
||||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
async_add_entities([HATextAISensor(coordinator, entry)], True)
|
instance_name = coordinator.instance_name
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Setting up sensor with instance: {instance_name}")
|
||||||
|
|
||||||
|
sensor = HATextAISensor(coordinator, entry)
|
||||||
|
async_add_entities([sensor], True)
|
||||||
|
|
||||||
class HATextAISensor(CoordinatorEntity, SensorEntity):
|
class HATextAISensor(CoordinatorEntity, SensorEntity):
|
||||||
"""HA text AI Sensor."""
|
"""HA Text AI Sensor."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
coordinator: HATextAICoordinator
|
||||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
|
||||||
_attr_device_class = SensorDeviceClass.TIMESTAMP
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -73,163 +90,205 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self._config_entry = config_entry
|
self._config_entry = config_entry
|
||||||
|
self._instance_name = coordinator.instance_name
|
||||||
|
self._conversation_history = []
|
||||||
|
self._system_prompt = None
|
||||||
|
|
||||||
|
self._attr_name = f"HA Text AI {self._instance_name}"
|
||||||
|
self.entity_id = f"sensor.ha_text_ai_{slugify(self._instance_name)}"
|
||||||
self._attr_unique_id = f"{config_entry.entry_id}"
|
self._attr_unique_id = f"{config_entry.entry_id}"
|
||||||
self._attr_name = "Last Response"
|
|
||||||
self._attr_suggested_display_precision = 0
|
self.entity_description = SensorEntityDescription(
|
||||||
|
key=f"ha_text_ai_{self._instance_name}",
|
||||||
|
entity_registry_enabled_default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._current_state = STATE_INITIALIZING
|
||||||
self._error_count = 0
|
self._error_count = 0
|
||||||
self._last_error = None
|
self._last_error = None
|
||||||
self._state = STATE_INITIALIZING
|
self._last_update = None
|
||||||
self._attr_device_info = {
|
self._is_processing = False
|
||||||
"identifiers": {(DOMAIN, self._attr_unique_id)},
|
self._last_response = {}
|
||||||
"name": "HA Text AI",
|
self._metrics = {}
|
||||||
"manufacturer": "Community",
|
|
||||||
"model": coordinator.model,
|
|
||||||
"sw_version": coordinator.api_version,
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
model = config_entry.data.get(CONF_MODEL, "Unknown")
|
||||||
def icon(self) -> str:
|
api_provider = config_entry.data.get(CONF_API_PROVIDER, "Unknown")
|
||||||
"""Return the icon based on the current state."""
|
|
||||||
if self._state == STATE_PROCESSING:
|
|
||||||
return ENTITY_ICON_PROCESSING
|
|
||||||
elif self._state in [STATE_ERROR, STATE_DISCONNECTED, STATE_RATE_LIMITED]:
|
|
||||||
return ENTITY_ICON_ERROR
|
|
||||||
return ENTITY_ICON
|
|
||||||
|
|
||||||
@property
|
self._attr_device_info = DeviceInfo(
|
||||||
def state(self) -> StateType:
|
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||||
"""Return the state of the sensor."""
|
name=self._attr_name, # Используем имя сенсора
|
||||||
if not self.coordinator.data or not self.coordinator.last_update_success:
|
manufacturer="Community",
|
||||||
return None
|
model=f"{model} ({api_provider} provider)",
|
||||||
|
sw_version="1.0.0",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
_LOGGER.debug(f"Initialized sensor: {self.entity_id} for instance: {self._instance_name}")
|
||||||
if self.coordinator.data and isinstance(self.coordinator.data, dict):
|
|
||||||
last_update = self.coordinator.data.get("last_update")
|
|
||||||
if isinstance(last_update, datetime):
|
|
||||||
return dt_util.as_local(last_update)
|
|
||||||
return last_update
|
|
||||||
return None
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error getting state: %s", err, exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def extra_state_attributes(self) -> Dict[str, Any]:
|
|
||||||
"""Return entity specific state attributes."""
|
|
||||||
attributes = {
|
|
||||||
ATTR_TOTAL_RESPONSES: 0,
|
|
||||||
ATTR_MODEL: self.coordinator.model,
|
|
||||||
ATTR_TEMPERATURE: self.coordinator.temperature,
|
|
||||||
ATTR_MAX_TOKENS: self.coordinator.max_tokens,
|
|
||||||
ATTR_SYSTEM_PROMPT: self.coordinator.system_prompt,
|
|
||||||
ATTR_QUEUE_SIZE: self.coordinator._question_queue.qsize(),
|
|
||||||
ATTR_API_STATUS: self._state,
|
|
||||||
ATTR_ERROR_COUNT: self._error_count,
|
|
||||||
ATTR_LAST_ERROR: self._last_error,
|
|
||||||
ATTR_API_VERSION: self.coordinator.api_version,
|
|
||||||
ATTR_ENDPOINT_STATUS: self.coordinator.endpoint_status,
|
|
||||||
ATTR_REQUEST_COUNT: self.coordinator.request_count,
|
|
||||||
ATTR_TOKENS_USED: self.coordinator.tokens_used,
|
|
||||||
}
|
|
||||||
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
try:
|
|
||||||
history = list(self.coordinator._responses.items())
|
|
||||||
if history:
|
|
||||||
last_question, last_data = history[-1]
|
|
||||||
|
|
||||||
if isinstance(last_data, dict):
|
|
||||||
last_response = last_data.get("response", "")
|
|
||||||
last_updated = last_data.get("timestamp") or self.coordinator.data.get("last_update")
|
|
||||||
response_time = last_data.get("response_time")
|
|
||||||
|
|
||||||
model = last_data.get("model", self.coordinator.model)
|
|
||||||
temperature = last_data.get("temperature", self.coordinator.temperature)
|
|
||||||
max_tokens = last_data.get("max_tokens", self.coordinator.max_tokens)
|
|
||||||
error = last_data.get("error")
|
|
||||||
|
|
||||||
if error:
|
|
||||||
self._last_error = error
|
|
||||||
self._state = STATE_ERROR
|
|
||||||
else:
|
|
||||||
last_response = str(last_data)
|
|
||||||
last_updated = self.coordinator.data.get("last_update")
|
|
||||||
response_time = None
|
|
||||||
model = self.coordinator.model
|
|
||||||
temperature = self.coordinator.temperature
|
|
||||||
max_tokens = self.coordinator.max_tokens
|
|
||||||
|
|
||||||
if isinstance(last_updated, datetime):
|
|
||||||
last_updated = dt_util.as_local(last_updated)
|
|
||||||
|
|
||||||
attributes.update({
|
|
||||||
ATTR_QUESTION: last_question,
|
|
||||||
ATTR_RESPONSE: last_response,
|
|
||||||
ATTR_LAST_UPDATED: last_updated,
|
|
||||||
ATTR_TOTAL_RESPONSES: len(history),
|
|
||||||
ATTR_MODEL: model,
|
|
||||||
ATTR_TEMPERATURE: temperature,
|
|
||||||
ATTR_MAX_TOKENS: max_tokens,
|
|
||||||
})
|
|
||||||
|
|
||||||
if response_time is not None:
|
|
||||||
attributes[ATTR_RESPONSE_TIME] = response_time
|
|
||||||
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error getting attributes: %s", err, exc_info=True)
|
|
||||||
self._error_count += 1
|
|
||||||
self._last_error = str(err)
|
|
||||||
self._state = STATE_ERROR
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return if entity is available."""
|
"""Return if entity is available."""
|
||||||
return self.coordinator.last_update_success
|
return (
|
||||||
|
self.coordinator.last_update_success
|
||||||
|
and self.coordinator.data is not None
|
||||||
|
and self._current_state != STATE_DISCONNECTED
|
||||||
|
)
|
||||||
|
|
||||||
|
def _sanitize_value(self, value: Any) -> Any:
|
||||||
|
"""Sanitize values for JSON serialization."""
|
||||||
|
if isinstance(value, float):
|
||||||
|
if math.isinf(value) or math.isnan(value):
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _sanitize_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Sanitize all attributes for JSON serialization."""
|
||||||
|
return {
|
||||||
|
key: self._sanitize_value(value)
|
||||||
|
for key, value in attributes.items()
|
||||||
|
if value is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the native value of the sensor."""
|
||||||
|
if not self.coordinator.last_update_success or not self.coordinator.data:
|
||||||
|
self._current_state = STATE_DISCONNECTED
|
||||||
|
return self._current_state
|
||||||
|
|
||||||
|
status = self.coordinator.data.get("state", STATE_READY)
|
||||||
|
self._current_state = status
|
||||||
|
return status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
"""Return the icon based on the current state."""
|
||||||
|
if self._current_state == STATE_ERROR:
|
||||||
|
return ENTITY_ICON_ERROR
|
||||||
|
elif self._current_state == STATE_PROCESSING:
|
||||||
|
return ENTITY_ICON_PROCESSING
|
||||||
|
return ENTITY_ICON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> Dict[str, Any]:
|
||||||
|
"""Return entity specific state attributes."""
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.coordinator.data
|
||||||
|
attributes = {
|
||||||
|
ATTR_MODEL: self._config_entry.data.get(CONF_MODEL, "Unknown"),
|
||||||
|
ATTR_API_PROVIDER: self._config_entry.data.get(CONF_API_PROVIDER, "Unknown"),
|
||||||
|
ATTR_API_STATUS: self._current_state,
|
||||||
|
ATTR_TOTAL_ERRORS: self._error_count,
|
||||||
|
ATTR_LAST_ERROR: self._last_error,
|
||||||
|
"instance_name": self._instance_name,
|
||||||
|
ATTR_SYSTEM_PROMPT: data.get("system_prompt"),
|
||||||
|
ATTR_IS_PROCESSING: data.get("is_processing", False),
|
||||||
|
ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False),
|
||||||
|
ATTR_IS_MAINTENANCE: data.get("is_maintenance", False),
|
||||||
|
ATTR_ENDPOINT_STATUS: data.get("endpoint_status", "unknown"),
|
||||||
|
ATTR_UPTIME: data.get("uptime", 0),
|
||||||
|
ATTR_HISTORY_SIZE: data.get("history_size", 0),
|
||||||
|
ATTR_CONVERSATION_HISTORY: data.get("conversation_history", []),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add metrics
|
||||||
|
metrics = data.get("metrics", {})
|
||||||
|
if isinstance(metrics, dict):
|
||||||
|
self._metrics = metrics
|
||||||
|
attributes.update({
|
||||||
|
METRIC_TOTAL_TOKENS: metrics.get("total_tokens", 0),
|
||||||
|
METRIC_PROMPT_TOKENS: metrics.get("prompt_tokens", 0),
|
||||||
|
METRIC_COMPLETION_TOKENS: metrics.get("completion_tokens", 0),
|
||||||
|
METRIC_SUCCESSFUL_REQUESTS: metrics.get("successful_requests", 0),
|
||||||
|
METRIC_FAILED_REQUESTS: metrics.get("failed_requests", 0),
|
||||||
|
METRIC_AVERAGE_LATENCY: metrics.get("average_latency", 0),
|
||||||
|
METRIC_MAX_LATENCY: metrics.get("max_latency", 0),
|
||||||
|
METRIC_MIN_LATENCY: metrics.get("min_latency", float("inf")),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add last response
|
||||||
|
last_response = data.get("last_response", {})
|
||||||
|
if isinstance(last_response, dict):
|
||||||
|
self._last_response = last_response
|
||||||
|
attributes.update({
|
||||||
|
ATTR_RESPONSE: last_response.get("response", ""),
|
||||||
|
ATTR_QUESTION: last_response.get("question", ""),
|
||||||
|
"last_model": last_response.get("model", ""),
|
||||||
|
"last_timestamp": last_response.get("timestamp", ""),
|
||||||
|
"last_error": last_response.get("error"),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add performance metrics if available
|
||||||
|
if ATTR_PERFORMANCE_METRICS in data:
|
||||||
|
attributes[ATTR_PERFORMANCE_METRICS] = data[ATTR_PERFORMANCE_METRICS]
|
||||||
|
|
||||||
|
# Add API version if available
|
||||||
|
if ATTR_API_VERSION in data:
|
||||||
|
attributes[ATTR_API_VERSION] = data[ATTR_API_VERSION]
|
||||||
|
|
||||||
|
return self._sanitize_attributes(attributes)
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error preparing attributes: %s", err, exc_info=True)
|
||||||
|
return {}
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""When entity is added to hass."""
|
"""When entity is added to hass."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
self._handle_coordinator_update()
|
self._handle_coordinator_update()
|
||||||
self._state = STATE_READY
|
_LOGGER.debug(f"Entity {self.entity_id} added to Home Assistant")
|
||||||
|
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Handle updated data from the coordinator."""
|
"""Handle updated data from the coordinator."""
|
||||||
try:
|
try:
|
||||||
if self.coordinator.data:
|
data = self.coordinator.data
|
||||||
if self.coordinator._is_ready:
|
if not self.coordinator.last_update_success or not data:
|
||||||
if self.coordinator._is_processing:
|
self._current_state = STATE_DISCONNECTED
|
||||||
self._state = STATE_PROCESSING
|
_LOGGER.warning(f"No data available for {self.entity_id}")
|
||||||
elif self.coordinator._is_rate_limited:
|
self.async_write_ha_state()
|
||||||
self._state = STATE_RATE_LIMITED
|
return
|
||||||
elif self.coordinator._is_maintenance:
|
|
||||||
self._state = STATE_MAINTENANCE
|
|
||||||
else:
|
|
||||||
self._state = STATE_READY
|
|
||||||
else:
|
|
||||||
self._state = STATE_DISCONNECTED
|
|
||||||
else:
|
|
||||||
self._state = STATE_DISCONNECTED
|
|
||||||
|
|
||||||
# Обновляем счетчик ошибок только если статус изменился на ошибку
|
self._is_processing = data.get("is_processing", False)
|
||||||
if self._state == STATE_ERROR:
|
|
||||||
|
# Update conversation history and system prompt
|
||||||
|
self._conversation_history = data.get("conversation_history", [])
|
||||||
|
self._system_prompt = data.get("system_prompt")
|
||||||
|
|
||||||
|
# Update state based on conditions
|
||||||
|
if self._is_processing:
|
||||||
|
self._current_state = STATE_PROCESSING
|
||||||
|
elif data.get("is_rate_limited"):
|
||||||
|
self._current_state = STATE_RATE_LIMITED
|
||||||
|
elif data.get("is_maintenance"):
|
||||||
|
self._current_state = STATE_MAINTENANCE
|
||||||
|
elif data.get("error"):
|
||||||
|
self._current_state = STATE_ERROR
|
||||||
|
self._last_error = data["error"]
|
||||||
self._error_count += 1
|
self._error_count += 1
|
||||||
|
else:
|
||||||
|
self._current_state = data.get("state", STATE_READY)
|
||||||
|
|
||||||
|
# Update last update timestamp
|
||||||
|
self._last_update = dt_util.utcnow()
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
f"Updated {self.entity_id} state to: {self._current_state} "
|
||||||
|
f"(available: {self.available})"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error handling update: %s", err, exc_info=True)
|
self._current_state = STATE_ERROR
|
||||||
self._error_count += 1
|
|
||||||
self._last_error = str(err)
|
self._last_error = str(err)
|
||||||
self._state = STATE_ERROR
|
self._error_count += 1
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error handling update for %s: %s",
|
||||||
|
self.entity_id,
|
||||||
|
err,
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_reset_error_count(self) -> None:
|
|
||||||
"""Reset the error counter."""
|
|
||||||
self._error_count = 0
|
|
||||||
self._last_error = None
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|||||||
@@ -3,69 +3,56 @@ ask_question:
|
|||||||
description: >-
|
description: >-
|
||||||
Send a question to the AI model and receive a detailed response.
|
Send a question to the AI model and receive a detailed response.
|
||||||
The response will be stored in the conversation history and can be retrieved later.
|
The response will be stored in the conversation history and can be retrieved later.
|
||||||
Response time may vary based on model selection and server load.
|
|
||||||
Supports various AI models with different capabilities and pricing.
|
|
||||||
fields:
|
fields:
|
||||||
|
instance:
|
||||||
|
name: Instance
|
||||||
|
description: Name of the HA Text AI instance to use
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
integration: ha_text_ai
|
||||||
|
domain: sensor
|
||||||
|
|
||||||
question:
|
question:
|
||||||
name: Question
|
name: Question
|
||||||
description: >-
|
description: Your question or prompt for the AI assistant
|
||||||
Your question or prompt for the AI assistant. Be specific and clear for better results.
|
|
||||||
You can ask about home automation, technical advice, or general questions.
|
|
||||||
For complex queries, consider breaking them into smaller parts.
|
|
||||||
The system will maintain conversation context for follow-up questions.
|
|
||||||
required: true
|
required: true
|
||||||
example: |
|
|
||||||
What automations would you recommend for a smart kitchen?
|
|
||||||
Consider energy efficiency, convenience, and integration with:
|
|
||||||
- Smart lighting
|
|
||||||
- Appliance control
|
|
||||||
- Temperature monitoring
|
|
||||||
- Voice commands
|
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
multiline: true
|
multiline: true
|
||||||
type: text
|
type: text
|
||||||
|
|
||||||
|
system_prompt:
|
||||||
|
name: System Prompt
|
||||||
|
description: Optional system prompt to set context for this specific question
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
multiline: true
|
||||||
|
|
||||||
|
context_messages:
|
||||||
|
name: Context Messages
|
||||||
|
description: Number of previous messages to include in context (1-20)
|
||||||
|
required: false
|
||||||
|
default: 5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 20
|
||||||
|
step: 1
|
||||||
|
mode: box
|
||||||
|
|
||||||
model:
|
model:
|
||||||
name: Model
|
name: Model
|
||||||
description: >-
|
description: "Select AI model to use (optional, overrides default setting)"
|
||||||
Select an AI model to use (optional, overrides default setting).
|
|
||||||
Different models have different capabilities, context limits, and response characteristics:
|
|
||||||
- GPT-3.5 Turbo: Fast, efficient, good for most tasks
|
|
||||||
- GPT-4: More capable, better reasoning, slower
|
|
||||||
- Claude-3: Advanced capabilities, longer context
|
|
||||||
Note: More capable models may have longer response times and higher API costs.
|
|
||||||
required: false
|
required: false
|
||||||
example: "gpt-3.5-turbo"
|
|
||||||
default: "gpt-3.5-turbo"
|
|
||||||
selector:
|
selector:
|
||||||
select:
|
text:
|
||||||
custom_value: true
|
multiline: false
|
||||||
options:
|
|
||||||
- label: "GPT-3.5 Turbo (Fast & Efficient)"
|
|
||||||
value: "gpt-3.5-turbo"
|
|
||||||
- label: "GPT-3.5 Turbo 16K (Extended)"
|
|
||||||
value: "gpt-3.5-turbo-16k"
|
|
||||||
- label: "GPT-4 (Most Capable)"
|
|
||||||
value: "gpt-4"
|
|
||||||
- label: "GPT-4 32K (Extended Context)"
|
|
||||||
value: "gpt-4-32k"
|
|
||||||
- label: "GPT-4 Turbo (Latest)"
|
|
||||||
value: "gpt-4-1106-preview"
|
|
||||||
- label: "Claude-3 Sonnet (Balanced)"
|
|
||||||
value: "claude-3-sonnet"
|
|
||||||
- label: "Claude-3 Opus (Most Advanced)"
|
|
||||||
value: "claude-3-opus"
|
|
||||||
mode: dropdown
|
|
||||||
|
|
||||||
temperature:
|
temperature:
|
||||||
name: Temperature
|
name: Temperature
|
||||||
description: >-
|
description: Controls response creativity (0.0-2.0)
|
||||||
Controls response creativity (0.0-2.0):
|
|
||||||
0.0-0.3: Focused, consistent responses (best for technical/factual queries)
|
|
||||||
0.4-0.7: Balanced responses (recommended for most uses)
|
|
||||||
0.8-2.0: More creative, varied responses (best for brainstorming)
|
|
||||||
Note: Higher values may produce less predictable results.
|
|
||||||
required: false
|
required: false
|
||||||
default: 0.7
|
default: 0.7
|
||||||
selector:
|
selector:
|
||||||
@@ -74,17 +61,10 @@ ask_question:
|
|||||||
max: 2.0
|
max: 2.0
|
||||||
step: 0.1
|
step: 0.1
|
||||||
mode: slider
|
mode: slider
|
||||||
unit_of_measurement: ""
|
|
||||||
|
|
||||||
max_tokens:
|
max_tokens:
|
||||||
name: Max Tokens
|
name: Max Tokens
|
||||||
description: >-
|
description: Maximum length of the response (1-4096 tokens)
|
||||||
Maximum length of the response (1-4096 tokens).
|
|
||||||
Recommended ranges:
|
|
||||||
- Short (256-512): Quick answers, status updates
|
|
||||||
- Medium (512-1024): Detailed explanations
|
|
||||||
- Long (1024-4096): Complex analysis
|
|
||||||
Note: Token limits vary by model. Actual length may be shorter.
|
|
||||||
required: false
|
required: false
|
||||||
default: 1000
|
default: 1000
|
||||||
selector:
|
selector:
|
||||||
@@ -93,113 +73,3 @@ ask_question:
|
|||||||
max: 4096
|
max: 4096
|
||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
|
|
||||||
system_prompt:
|
|
||||||
name: System Prompt
|
|
||||||
description: >-
|
|
||||||
Optional system prompt to set context for this specific question.
|
|
||||||
This will temporarily override the default system prompt.
|
|
||||||
Use this to specify expertise areas, response format, or special instructions.
|
|
||||||
required: false
|
|
||||||
example: "You are a home automation expert focused on energy efficiency and security"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
multiline: true
|
|
||||||
|
|
||||||
clear_history:
|
|
||||||
name: Clear History
|
|
||||||
description: >-
|
|
||||||
Delete all stored questions and responses from the conversation history.
|
|
||||||
This action cannot be undone. Consider using 'get_history' first if you need to backup the data.
|
|
||||||
System prompt settings and configuration will be preserved.
|
|
||||||
fields: {}
|
|
||||||
|
|
||||||
get_history:
|
|
||||||
name: Get History
|
|
||||||
description: >-
|
|
||||||
Retrieve recent conversation history, including:
|
|
||||||
- Questions and responses
|
|
||||||
- Timestamps and response times
|
|
||||||
- Models used and token counts
|
|
||||||
- Temperature and other settings
|
|
||||||
Results are ordered from newest to oldest.
|
|
||||||
fields:
|
|
||||||
limit:
|
|
||||||
name: Limit
|
|
||||||
description: >-
|
|
||||||
Number of most recent conversations to return (1-100).
|
|
||||||
Higher values return more history but may take longer to process.
|
|
||||||
Default: 10 conversations
|
|
||||||
required: false
|
|
||||||
default: 10
|
|
||||||
selector:
|
|
||||||
number:
|
|
||||||
min: 1
|
|
||||||
max: 100
|
|
||||||
step: 1
|
|
||||||
mode: box
|
|
||||||
|
|
||||||
filter_model:
|
|
||||||
name: Filter by Model
|
|
||||||
description: >-
|
|
||||||
Only return conversations using a specific AI model.
|
|
||||||
Leave empty to show all models.
|
|
||||||
required: false
|
|
||||||
selector:
|
|
||||||
select:
|
|
||||||
custom_value: true
|
|
||||||
options:
|
|
||||||
- label: "All Models"
|
|
||||||
value: ""
|
|
||||||
- label: "GPT-3.5 Turbo"
|
|
||||||
value: "gpt-3.5-turbo"
|
|
||||||
- label: "GPT-4"
|
|
||||||
value: "gpt-4"
|
|
||||||
- label: "GPT-4 Turbo"
|
|
||||||
value: "gpt-4-1106-preview"
|
|
||||||
- label: "Claude-3 Sonnet"
|
|
||||||
value: "claude-3-sonnet"
|
|
||||||
- label: "Claude-3 Opus"
|
|
||||||
value: "claude-3-opus"
|
|
||||||
mode: dropdown
|
|
||||||
|
|
||||||
start_date:
|
|
||||||
name: Start Date
|
|
||||||
description: >-
|
|
||||||
Optional start date for filtering history.
|
|
||||||
Format: YYYY-MM-DD
|
|
||||||
required: false
|
|
||||||
selector:
|
|
||||||
datetime:
|
|
||||||
type: date
|
|
||||||
|
|
||||||
set_system_prompt:
|
|
||||||
name: Set System Prompt
|
|
||||||
description: >-
|
|
||||||
Set default system behavior instructions for all future conversations.
|
|
||||||
This defines how the AI should behave and respond to questions.
|
|
||||||
The prompt will be used for all models unless overridden per question.
|
|
||||||
fields:
|
|
||||||
prompt:
|
|
||||||
name: System Prompt
|
|
||||||
description: >-
|
|
||||||
Instructions that define how the AI should behave and respond.
|
|
||||||
Be specific about:
|
|
||||||
- Desired expertise and knowledge areas
|
|
||||||
- Response tone and style
|
|
||||||
- Output format preferences
|
|
||||||
- Special handling instructions
|
|
||||||
Maximum length: 1000 characters
|
|
||||||
required: true
|
|
||||||
example: |
|
|
||||||
You are a home automation expert assistant. Focus on:
|
|
||||||
1. Practical and efficient solutions
|
|
||||||
2. Energy-saving recommendations
|
|
||||||
3. Integration with popular smart home platforms
|
|
||||||
4. Security and privacy considerations
|
|
||||||
Provide detailed but concise responses with clear steps when applicable.
|
|
||||||
Format complex responses with bullet points or numbered lists.
|
|
||||||
Include warnings about potential risks or limitations.
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
multiline: true
|
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"provider": {
|
||||||
|
"title": "KI-Anbieter auswählen",
|
||||||
|
"description": "Wählen Sie den KI-Dienst-Anbieter für diese Instanz",
|
||||||
|
"data": {
|
||||||
|
"api_provider": "API-Anbieter",
|
||||||
|
"context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "HA Text AI Instanz konfigurieren",
|
||||||
|
"description": "Richten Sie eine neue KI-Assistenten-Instanz mit Ihrem ausgewählten Anbieter ein",
|
||||||
|
"data": {
|
||||||
|
"name": "Instanzname (z.B. 'GPT Assistent', 'Claude Helfer')",
|
||||||
|
"api_key": "API-Schlüssel für Authentifizierung",
|
||||||
|
"model": "Zu verwendendes KI-Modell",
|
||||||
|
"temperature": "Antwort-Kreativität (0-2, niedriger = fokussierter)",
|
||||||
|
"max_tokens": "Maximale Antwortlänge (1-4096 Token)",
|
||||||
|
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
||||||
|
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
||||||
|
"context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"name_exists": "Eine Instanz mit diesem Namen existiert bereits",
|
||||||
|
"invalid_name": "Ungültiger Instanzname",
|
||||||
|
"invalid_auth": "Authentifizierung fehlgeschlagen - überprüfen Sie Ihren API-Schlüssel",
|
||||||
|
"invalid_api_key": "Ungültiger API-Schlüssel - bitte überprüfen Sie Ihre Anmeldedaten",
|
||||||
|
"cannot_connect": "Verbindung zum API-Dienst fehlgeschlagen",
|
||||||
|
"invalid_model": "Ausgewähltes Modell ist nicht verfügbar",
|
||||||
|
"rate_limit": "Anfragelimit überschritten",
|
||||||
|
"context_length": "Kontextlänge überschritten",
|
||||||
|
"rate_limit_exceeded": "API-Anfragelimit überschritten",
|
||||||
|
"maintenance": "Dienst ist in Wartung",
|
||||||
|
"invalid_response": "Ungültige API-Antwort erhalten",
|
||||||
|
"api_error": "API-Dienst-Fehler aufgetreten",
|
||||||
|
"timeout": "Anfrage-Zeitüberschreitung",
|
||||||
|
"invalid_instance": "Ungültige Instanz angegeben",
|
||||||
|
"unknown": "Unerwarteter Fehler aufgetreten"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Instanzeinstellungen aktualisieren",
|
||||||
|
"description": "Einstellungen für diese KI-Assistenten-Instanz ändern",
|
||||||
|
"data": {
|
||||||
|
"model": "KI-Modell",
|
||||||
|
"temperature": "Antwort-Kreativität (0-2)",
|
||||||
|
"max_tokens": "Maximale Antwortlänge (1-4096)",
|
||||||
|
"request_interval": "Minimales Anfragen-Intervall (0,1-60 Sekunden)",
|
||||||
|
"context_messages": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"ask_question": {
|
||||||
|
"name": "Frage stellen (HA Text AI)",
|
||||||
|
"description": "Senden Sie eine Frage an das KI-Modell und erhalten Sie eine detaillierte Antwort. Die Antwort wird in der Gesprächshistorie gespeichert und kann später abgerufen werden.",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instanz",
|
||||||
|
"description": "Name der zu verwendenden HA Text AI Instanz"
|
||||||
|
},
|
||||||
|
"question": {
|
||||||
|
"name": "Frage",
|
||||||
|
"description": "Ihre Frage oder Eingabeaufforderung für den KI-Assistenten"
|
||||||
|
},
|
||||||
|
"context_messages": {
|
||||||
|
"name": "Kontextnachrichten",
|
||||||
|
"description": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)"
|
||||||
|
},
|
||||||
|
"system_prompt": {
|
||||||
|
"name": "Systemaufforderung",
|
||||||
|
"description": "Optionale Systemaufforderung zur Kontexteinstellung für diese spezifische Frage"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "Modell",
|
||||||
|
"description": "Wählen Sie das zu verwendende KI-Modell (optional, überschreibt Standardeinstellung)"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"name": "Temperatur",
|
||||||
|
"description": "Steuert die Antwort-Kreativität (0,0-2,0)"
|
||||||
|
},
|
||||||
|
"max_tokens": {
|
||||||
|
"name": "Max. Token",
|
||||||
|
"description": "Maximale Länge der Antwort (1-4096 Token)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clear_history": {
|
||||||
|
"name": "Verlauf löschen",
|
||||||
|
"description": "Alle gespeicherten Fragen und Antworten aus dem Gesprächsverlauf löschen",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instanz",
|
||||||
|
"description": "Name der HA Text AI Instanz, für die der Verlauf gelöscht werden soll"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get_history": {
|
||||||
|
"name": "Verlauf abrufen",
|
||||||
|
"description": "Gesprächsverlauf mit optionaler Filterung und Sortierung abrufen",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instanz",
|
||||||
|
"description": "Name der HA Text AI Instanz, aus der der Verlauf abgerufen werden soll"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"name": "Limit",
|
||||||
|
"description": "Anzahl der zurückzugebenden Gespräche (1-100)"
|
||||||
|
},
|
||||||
|
"filter_model": {
|
||||||
|
"name": "Modell filtern",
|
||||||
|
"description": "Gespräche nach bestimmtem KI-Modell filtern"
|
||||||
|
},
|
||||||
|
"start_date": {
|
||||||
|
"name": "Startdatum",
|
||||||
|
"description": "Gespräche ab diesem Datum/Zeitpunkt filtern"
|
||||||
|
},
|
||||||
|
"include_metadata": {
|
||||||
|
"name": "Metadaten einbeziehen",
|
||||||
|
"description": "Zusätzliche Informationen wie verwendete Token, Antwortzeit usw. einbeziehen"
|
||||||
|
},
|
||||||
|
"sort_order": {
|
||||||
|
"name": "Sortierreihenfolge",
|
||||||
|
"description": "Sortierreihenfolge der Ergebnisse (neueste oder älteste zuerst)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"set_system_prompt": {
|
||||||
|
"name": "Systemaufforderung festlegen",
|
||||||
|
"description": "Standardmäßige Systemverhaltensinstruktionen für alle zukünftigen Gespräche festlegen",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instanz",
|
||||||
|
"description": "Name der HA Text AI Instanz, für die die Systemaufforderung festgelegt werden soll"
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"name": "Systemaufforderung",
|
||||||
|
"description": "Anweisungen, die definieren, wie sich die KI verhalten und antworten soll"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"ha_text_ai": {
|
||||||
|
"name": "{name}",
|
||||||
|
"state": {
|
||||||
|
"ready": "Bereit",
|
||||||
|
"processing": "Verarbeitung",
|
||||||
|
"error": "Fehler",
|
||||||
|
"disconnected": "Getrennt",
|
||||||
|
"rate_limited": "Anfragelimit",
|
||||||
|
"maintenance": "Wartung",
|
||||||
|
"initializing": "Initialisierung",
|
||||||
|
"retrying": "Wiederholung",
|
||||||
|
"queued": "In Warteschlange"
|
||||||
|
},
|
||||||
|
"state_attributes": {
|
||||||
|
"question": {
|
||||||
|
"name": "Letzte Frage"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"name": "Letzte Antwort"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "Aktuelles Modell"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"name": "Temperatur"
|
||||||
|
},
|
||||||
|
"max_tokens": {
|
||||||
|
"name": "Max. Token"
|
||||||
|
},
|
||||||
|
"system_prompt": {
|
||||||
|
"name": "Systemaufforderung"
|
||||||
|
},
|
||||||
|
"response_time": {
|
||||||
|
"name": "Letzte Antwortzeit"
|
||||||
|
},
|
||||||
|
"total_responses": {
|
||||||
|
"name": "Gesamte Antworten"
|
||||||
|
},
|
||||||
|
"error_count": {
|
||||||
|
"name": "Fehleranzahl"
|
||||||
|
},
|
||||||
|
"last_error": {
|
||||||
|
"name": "Letzter Fehler"
|
||||||
|
},
|
||||||
|
"api_status": {
|
||||||
|
"name": "API-Status"
|
||||||
|
},
|
||||||
|
"tokens_used": {
|
||||||
|
"name": "Insgesamt verwendete Token"
|
||||||
|
},
|
||||||
|
"average_response_time": {
|
||||||
|
"name": "Durchschnittliche Antwortzeit"
|
||||||
|
},
|
||||||
|
"last_request_time": {
|
||||||
|
"name": "Letzte Anforderungszeit"
|
||||||
|
},
|
||||||
|
"is_processing": {
|
||||||
|
"name": "Verarbeitungsstatus"
|
||||||
|
},
|
||||||
|
"is_rate_limited": {
|
||||||
|
"name": "Status Anfragelimit"
|
||||||
|
},
|
||||||
|
"is_maintenance": {
|
||||||
|
"name": "Wartungsstatus"
|
||||||
|
},
|
||||||
|
"api_version": {
|
||||||
|
"name": "API-Version"
|
||||||
|
},
|
||||||
|
"endpoint_status": {
|
||||||
|
"name": "Endpunktstatus"
|
||||||
|
},
|
||||||
|
"performance_metrics": {
|
||||||
|
"name": "Leistungsmetriken"
|
||||||
|
},
|
||||||
|
"history_size": {
|
||||||
|
"name": "Verlaufsgröße"
|
||||||
|
},
|
||||||
|
"uptime": {
|
||||||
|
"name": "Betriebszeit"
|
||||||
|
},
|
||||||
|
"total_tokens": {
|
||||||
|
"name": "Gesamte Token"
|
||||||
|
},
|
||||||
|
"prompt_tokens": {
|
||||||
|
"name": "Prompt-Token"
|
||||||
|
},
|
||||||
|
"completion_tokens": {
|
||||||
|
"name": "Abschluss-Token"
|
||||||
|
},
|
||||||
|
"successful_requests": {
|
||||||
|
"name": "Erfolgreiche Anfragen"
|
||||||
|
},
|
||||||
|
"failed_requests": {
|
||||||
|
"name": "Fehlgeschlagene Anfragen"
|
||||||
|
},
|
||||||
|
"average_latency": {
|
||||||
|
"name": "Durchschnittliche Latenz"
|
||||||
|
},
|
||||||
|
"max_latency": {
|
||||||
|
"name": "Maximale Latenz"
|
||||||
|
},
|
||||||
|
"min_latency": {
|
||||||
|
"name": "Minimale Latenz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,145 +1,258 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"provider": {
|
||||||
"title": "Set up HA Text AI",
|
"title": "Select AI Provider",
|
||||||
"description": "Configure your AI integration for various providers (OpenAI, Anthropic, etc.). Different models have different capabilities and pricing.",
|
"description": "Choose which AI service provider to use for this instance",
|
||||||
"data": {
|
"data": {
|
||||||
"api_key": "API key for authentication (required)",
|
"api_provider": "API Provider",
|
||||||
"model": "AI model to use (e.g., gpt-3.5-turbo, gpt-4, claude-3-sonnet, claude-3-opus)",
|
"context_messages": "Number of context messages to retain (1-20)"
|
||||||
"temperature": "Response creativity (0-2, lower = more focused and consistent)",
|
}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "Configure HA Text AI Instance",
|
||||||
|
"description": "Set up a new AI assistant instance with your selected provider",
|
||||||
|
"data": {
|
||||||
|
"name": "Instance name (e.g., 'GPT Assistant', 'Claude Helper')",
|
||||||
|
"api_key": "API key for authentication",
|
||||||
|
"model": "AI model to use",
|
||||||
|
"temperature": "Response creativity (0-2, lower = more focused)",
|
||||||
"max_tokens": "Maximum response length (1-4096 tokens)",
|
"max_tokens": "Maximum response length (1-4096 tokens)",
|
||||||
"api_endpoint": "Custom API endpoint URL (default varies by provider)",
|
"api_endpoint": "Custom API endpoint URL (optional)",
|
||||||
"request_interval": "Minimum time between requests in seconds (min: 0.1)",
|
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
||||||
"system_prompt": "Default instructions for AI behavior and expertise"
|
"context_messages": "Number of context messages to retain (1-20)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"name_exists": "An instance with this name already exists",
|
||||||
|
"invalid_name": "Invalid instance name",
|
||||||
"invalid_auth": "Authentication failed - check your API key",
|
"invalid_auth": "Authentication failed - check your API key",
|
||||||
"invalid_api_key": "Invalid API key - please verify your credentials",
|
"invalid_api_key": "Invalid API key - please verify your credentials",
|
||||||
"cannot_connect": "Connection failed - check endpoint and network status",
|
"cannot_connect": "Failed to connect to API service",
|
||||||
"invalid_model": "Model unavailable or not supported by the API",
|
"invalid_model": "Selected model is not available",
|
||||||
"rate_limit": "Rate limit exceeded - please reduce request frequency",
|
"rate_limit": "Rate limit exceeded",
|
||||||
"context_length": "Input exceeds maximum context length for model",
|
"context_length": "Context length exceeded",
|
||||||
"api_error": "API service error - check provider status",
|
"rate_limit_exceeded": "API rate limit exceeded",
|
||||||
"timeout": "Request timeout - server not responding",
|
"maintenance": "Service is under maintenance",
|
||||||
"queue_full": "Request queue full - try again later",
|
"invalid_response": "Invalid API response received",
|
||||||
"invalid_prompt": "Invalid system prompt format or length",
|
"api_error": "API service error occurred",
|
||||||
"invalid_url_format": "Invalid API endpoint URL format",
|
"timeout": "Request timed out",
|
||||||
"invalid_input": "Invalid configuration parameters",
|
"invalid_instance": "Invalid instance specified",
|
||||||
"unknown": "Unexpected error - check logs for details"
|
"unknown": "Unexpected error occurred"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"title": "HA Text AI Settings",
|
"title": "Update Instance Settings",
|
||||||
"description": "Adjust your AI integration parameters and behavior",
|
"description": "Modify settings for this AI assistant instance",
|
||||||
"data": {
|
"data": {
|
||||||
"model": "Select AI model for responses (capabilities vary)",
|
"model": "AI model",
|
||||||
"temperature": "Response creativity (0-2, affects variation)",
|
"temperature": "Response creativity (0-2)",
|
||||||
"max_tokens": "Maximum response length in tokens",
|
"max_tokens": "Maximum response length (1-4096)",
|
||||||
"request_interval": "Minimum seconds between requests (rate limiting)",
|
"request_interval": "Minimum request interval (0.1-60 seconds)",
|
||||||
"system_prompt": "Default AI behavior and expertise instructions",
|
"context_messages": "Number of previous messages to include in context (1-20)"
|
||||||
"timeout": "Request timeout in seconds (default: 30)",
|
|
||||||
"retry_count": "Number of retry attempts for failed requests",
|
|
||||||
"queue_size": "Maximum pending requests in queue"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Ask Question",
|
"name": "Ask Question (HA Text AI)",
|
||||||
"description": "Send a question to the AI model and get a detailed response. Supports context awareness for follow-up questions.",
|
"description": "Send a question to the AI model and receive a detailed response. The response will be stored in the conversation history and can be retrieved later.",
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "Name of the HA Text AI instance to use"
|
||||||
|
},
|
||||||
"question": {
|
"question": {
|
||||||
"name": "Question",
|
"name": "Question",
|
||||||
"description": "Your question or prompt for the AI model. Be specific for better results."
|
"description": "Your question or prompt for the AI assistant"
|
||||||
|
},
|
||||||
|
"context_messages": {
|
||||||
|
"name": "Context Messages",
|
||||||
|
"description": "Number of previous messages to include in context (1-20)"
|
||||||
},
|
},
|
||||||
"system_prompt": {
|
"system_prompt": {
|
||||||
"name": "System Prompt",
|
"name": "System Prompt",
|
||||||
"description": "Optional behavior instructions for this specific question"
|
"description": "Optional system prompt to set context for this specific question"
|
||||||
},
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"name": "Model",
|
"name": "Model",
|
||||||
"description": "Optional specific AI model for this request (overrides default)"
|
"description": "Select AI model to use (optional, overrides default setting)"
|
||||||
},
|
},
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"name": "Temperature",
|
"name": "Temperature",
|
||||||
"description": "Optional creativity setting (0-2, affects response variation)"
|
"description": "Controls response creativity (0.0-2.0)"
|
||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Max Tokens",
|
"name": "Max Tokens",
|
||||||
"description": "Optional maximum response length (1-4096 tokens)"
|
"description": "Maximum length of the response (1-4096 tokens)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clear_history": {
|
"clear_history": {
|
||||||
"name": "Clear History",
|
"name": "Clear History",
|
||||||
"description": "Delete all stored conversation history, responses, and metadata"
|
"description": "Delete all stored questions and responses from the conversation history",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "Name of the HA Text AI instance to clear history for"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"get_history": {
|
"get_history": {
|
||||||
"name": "Get History",
|
"name": "Get History",
|
||||||
"description": "Retrieve conversation history with detailed metadata including timestamps",
|
"description": "Retrieve conversation history with optional filtering and sorting",
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "Name of the HA Text AI instance to get history from"
|
||||||
|
},
|
||||||
"limit": {
|
"limit": {
|
||||||
"name": "Limit",
|
"name": "Limit",
|
||||||
"description": "Maximum number of conversations to return (1-100)"
|
"description": "Number of conversations to return (1-100)"
|
||||||
},
|
},
|
||||||
"filter_model": {
|
"filter_model": {
|
||||||
"name": "Filter Model",
|
"name": "Filter Model",
|
||||||
"description": "Show only responses from a specific model"
|
"description": "Filter conversations by specific AI model"
|
||||||
},
|
},
|
||||||
"start_date": {
|
"start_date": {
|
||||||
"name": "Start Date",
|
"name": "Start Date",
|
||||||
"description": "Filter conversations from this date (YYYY-MM-DD)"
|
"description": "Filter conversations starting from this date/time"
|
||||||
|
},
|
||||||
|
"include_metadata": {
|
||||||
|
"name": "Include Metadata",
|
||||||
|
"description": "Include additional information like tokens used, response time, etc."
|
||||||
|
},
|
||||||
|
"sort_order": {
|
||||||
|
"name": "Sort Order",
|
||||||
|
"description": "Sort order for results (newest or oldest first)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"set_system_prompt": {
|
"set_system_prompt": {
|
||||||
"name": "Set System Prompt",
|
"name": "Set System Prompt",
|
||||||
"description": "Update default AI behavior and expertise instructions",
|
"description": "Set default system behavior instructions for all future conversations",
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "Name of the HA Text AI instance to set system prompt for"
|
||||||
|
},
|
||||||
"prompt": {
|
"prompt": {
|
||||||
"name": "System Prompt",
|
"name": "System Prompt",
|
||||||
"description": "Instructions for AI behavior, expertise, and response style"
|
"description": "Instructions that define how the AI should behave and respond"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"state": {
|
|
||||||
"ready": "Ready for requests",
|
|
||||||
"processing": "Processing request",
|
|
||||||
"error": "Error occurred",
|
|
||||||
"disconnected": "API disconnected",
|
|
||||||
"rate_limited": "Rate limit reached",
|
|
||||||
"initializing": "Starting up",
|
|
||||||
"retrying": "Retrying request",
|
|
||||||
"queued": "Request queued"
|
|
||||||
},
|
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"status": {
|
"ha_text_ai": {
|
||||||
"name": "AI Status",
|
"name": "{name}",
|
||||||
"state": {
|
"state": {
|
||||||
"ready": "Ready",
|
"ready": "Ready",
|
||||||
"processing": "Processing",
|
"processing": "Processing",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"disconnected": "Disconnected",
|
"disconnected": "Disconnected",
|
||||||
"rate_limited": "Rate Limited",
|
"rate_limited": "Rate Limited",
|
||||||
|
"maintenance": "Maintenance",
|
||||||
"initializing": "Initializing",
|
"initializing": "Initializing",
|
||||||
"retrying": "Retrying",
|
"retrying": "Retrying",
|
||||||
"queued": "Queued"
|
"queued": "Queued"
|
||||||
}
|
},
|
||||||
},
|
"state_attributes": {
|
||||||
"last_response": {
|
"question": {
|
||||||
"name": "Last Response",
|
"name": "Last Question"
|
||||||
"state": {
|
},
|
||||||
"success": "Success",
|
"response": {
|
||||||
"error": "Error",
|
"name": "Last Response"
|
||||||
"timeout": "Timeout"
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "Current Model"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"name": "Temperature"
|
||||||
|
},
|
||||||
|
"max_tokens": {
|
||||||
|
"name": "Max Tokens"
|
||||||
|
},
|
||||||
|
"system_prompt": {
|
||||||
|
"name": "System Prompt"
|
||||||
|
},
|
||||||
|
"response_time": {
|
||||||
|
"name": "Last Response Time"
|
||||||
|
},
|
||||||
|
"total_responses": {
|
||||||
|
"name": "Total Responses"
|
||||||
|
},
|
||||||
|
"error_count": {
|
||||||
|
"name": "Error Count"
|
||||||
|
},
|
||||||
|
"last_error": {
|
||||||
|
"name": "Last Error"
|
||||||
|
},
|
||||||
|
"api_status": {
|
||||||
|
"name": "API Status"
|
||||||
|
},
|
||||||
|
"tokens_used": {
|
||||||
|
"name": "Total Tokens Used"
|
||||||
|
},
|
||||||
|
"average_response_time": {
|
||||||
|
"name": "Average Response Time"
|
||||||
|
},
|
||||||
|
"last_request_time": {
|
||||||
|
"name": "Last Request Time"
|
||||||
|
},
|
||||||
|
"is_processing": {
|
||||||
|
"name": "Processing Status"
|
||||||
|
},
|
||||||
|
"is_rate_limited": {
|
||||||
|
"name": "Rate Limited Status"
|
||||||
|
},
|
||||||
|
"is_maintenance": {
|
||||||
|
"name": "Maintenance Status"
|
||||||
|
},
|
||||||
|
"api_version": {
|
||||||
|
"name": "API Version"
|
||||||
|
},
|
||||||
|
"endpoint_status": {
|
||||||
|
"name": "Endpoint Status"
|
||||||
|
},
|
||||||
|
"performance_metrics": {
|
||||||
|
"name": "Performance Metrics"
|
||||||
|
},
|
||||||
|
"history_size": {
|
||||||
|
"name": "History Size"
|
||||||
|
},
|
||||||
|
"uptime": {
|
||||||
|
"name": "Uptime"
|
||||||
|
},
|
||||||
|
"total_tokens": {
|
||||||
|
"name": "Total Tokens"
|
||||||
|
},
|
||||||
|
"prompt_tokens": {
|
||||||
|
"name": "Prompt Tokens"
|
||||||
|
},
|
||||||
|
"completion_tokens": {
|
||||||
|
"name": "Completion Tokens"
|
||||||
|
},
|
||||||
|
"successful_requests": {
|
||||||
|
"name": "Successful Requests"
|
||||||
|
},
|
||||||
|
"failed_requests": {
|
||||||
|
"name": "Failed Requests"
|
||||||
|
},
|
||||||
|
"average_latency": {
|
||||||
|
"name": "Average Latency"
|
||||||
|
},
|
||||||
|
"max_latency": {
|
||||||
|
"name": "Maximum Latency"
|
||||||
|
},
|
||||||
|
"min_latency": {
|
||||||
|
"name": "Minimum Latency"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"provider": {
|
||||||
|
"title": "Выбор провайдера ИИ",
|
||||||
|
"description": "Выберите сервис ИИ для этого экземпляра",
|
||||||
|
"data": {
|
||||||
|
"api_provider": "Провайдер API",
|
||||||
|
"context_messages": "Количество сообщений в контексте (1-20)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "Настройка экземпляра HA Text AI",
|
||||||
|
"description": "Настройте нового помощника ИИ с выбранным провайдером",
|
||||||
|
"data": {
|
||||||
|
"name": "Название экземпляра (например, 'GPT Помощник', 'Claude Ассистент')",
|
||||||
|
"api_key": "API-ключ для аутентификации",
|
||||||
|
"model": "Модель ИИ для использования",
|
||||||
|
"temperature": "Креативность ответов (0-2, меньше = более сфокусированно)",
|
||||||
|
"max_tokens": "Максимальная длина ответа (1-4096 токенов)",
|
||||||
|
"api_endpoint": "Пользовательский URL-адрес API (необязательно)",
|
||||||
|
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
||||||
|
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"name_exists": "Экземпляр с таким именем уже существует",
|
||||||
|
"invalid_name": "Некорректное имя экземпляра",
|
||||||
|
"invalid_auth": "Ошибка аутентификации - проверьте API-ключ",
|
||||||
|
"invalid_api_key": "Неверный API-ключ - проверьте учетные данные",
|
||||||
|
"cannot_connect": "Не удалось подключиться к сервису API",
|
||||||
|
"invalid_model": "Выбранная модель недоступна",
|
||||||
|
"rate_limit": "Превышен лимит запросов",
|
||||||
|
"context_length": "Превышена длина контекста",
|
||||||
|
"rate_limit_exceeded": "Превышен лимит API",
|
||||||
|
"maintenance": "Сервис на техническом обслуживании",
|
||||||
|
"invalid_response": "Получен некорректный ответ API",
|
||||||
|
"api_error": "Произошла ошибка сервиса API",
|
||||||
|
"timeout": "Время ожидания истекло",
|
||||||
|
"invalid_instance": "Указан неверный экземпляр",
|
||||||
|
"unknown": "Произошла непредвиденная ошибка"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Обновление настроек экземпляра",
|
||||||
|
"description": "Измените настройки для этого помощника ИИ",
|
||||||
|
"data": {
|
||||||
|
"model": "Модель ИИ",
|
||||||
|
"temperature": "Креативность ответов (0-2)",
|
||||||
|
"max_tokens": "Максимальная длина ответа (1-4096)",
|
||||||
|
"request_interval": "Минимальный интервал запросов (0.1-60 секунд)",
|
||||||
|
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"ask_question": {
|
||||||
|
"name": "Задать вопрос (HA Text AI)",
|
||||||
|
"description": "Отправьте вопрос модели ИИ и получите подробный ответ. Ответ будет сохранен в истории беседы и может быть извлечен позже.",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Экземпляр",
|
||||||
|
"description": "Название экземпляра HA Text AI для использования"
|
||||||
|
},
|
||||||
|
"question": {
|
||||||
|
"name": "Вопрос",
|
||||||
|
"description": "Ваш вопрос или запрос помощнику ИИ"
|
||||||
|
},
|
||||||
|
"context_messages": {
|
||||||
|
"name": "Контекстные сообщения",
|
||||||
|
"description": "Количество предыдущих сообщений для включения в контекст (1-20)"
|
||||||
|
},
|
||||||
|
"system_prompt": {
|
||||||
|
"name": "Системный промпт",
|
||||||
|
"description": "Необязательный системный промпт для установки контекста для этого конкретного вопроса"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "Модель",
|
||||||
|
"description": "Выберите модель ИИ (необязательно, переопределяет настройки по умолчанию)"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"name": "Температура",
|
||||||
|
"description": "Управляет креативностью ответа (0.0-2.0)"
|
||||||
|
},
|
||||||
|
"max_tokens": {
|
||||||
|
"name": "Максимальное количество токенов",
|
||||||
|
"description": "Максимальная длина ответа (1-4096 токенов)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clear_history": {
|
||||||
|
"name": "Очистить историю",
|
||||||
|
"description": "Удалить все сохраненные вопросы и ответы из истории беседы",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Экземпляр",
|
||||||
|
"description": "Название экземпляра HA Text AI для очистки истории"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get_history": {
|
||||||
|
"name": "Получить историю",
|
||||||
|
"description": "Получить историю беседы с дополнительной фильтрацией и сортировкой",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Экземпляр",
|
||||||
|
"description": "Название экземпляра HA Text AI для получения истории"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"name": "Лимит",
|
||||||
|
"description": "Количество возвращаемых бесед (1-100)"
|
||||||
|
},
|
||||||
|
"filter_model": {
|
||||||
|
"name": "Фильтр модели",
|
||||||
|
"description": "Фильтрация бесед по конкретной модели ИИ"
|
||||||
|
},
|
||||||
|
"start_date": {
|
||||||
|
"name": "Начальная дата",
|
||||||
|
"description": "Фильтрация бесед, начиная с указанной даты/времени"
|
||||||
|
},
|
||||||
|
"include_metadata": {
|
||||||
|
"name": "Включить метаданные",
|
||||||
|
"description": "Включить дополнительную информацию, например, использованные токены, время ответа и т.д."
|
||||||
|
},
|
||||||
|
"sort_order": {
|
||||||
|
"name": "Порядок сортировки",
|
||||||
|
"description": "Порядок результатов (сначала новые или старые)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"set_system_prompt": {
|
||||||
|
"name": "Установить системный промпт",
|
||||||
|
"description": "Установить инструкции по умолчанию для поведения ИИ во всех будущих разговорах",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Экземпляр",
|
||||||
|
"description": "Название экземпляра HA Text AI для установки системного промпта"
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"name": "Системный промпт",
|
||||||
|
"description": "Инструкции, определяющие, как ИИ должен вести себя и отвечать"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"ha_text_ai": {
|
||||||
|
"name": "{name}",
|
||||||
|
"state": {
|
||||||
|
"ready": "Готов",
|
||||||
|
"processing": "Обработка",
|
||||||
|
"error": "Ошибка",
|
||||||
|
"disconnected": "Отключен",
|
||||||
|
"rate_limited": "Лимит запросов",
|
||||||
|
"maintenance": "Техническое обслуживание",
|
||||||
|
"initializing": "Инициализация",
|
||||||
|
"retrying": "Повторная попытка",
|
||||||
|
"queued": "В очереди"
|
||||||
|
},
|
||||||
|
"state_attributes": {
|
||||||
|
"question": {
|
||||||
|
"name": "Последний вопрос"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"name": "Последний ответ"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "Текущая модель"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"name": "Температура"
|
||||||
|
},
|
||||||
|
"max_tokens": {
|
||||||
|
"name": "Максимальное количество токенов"
|
||||||
|
},
|
||||||
|
"system_prompt": {
|
||||||
|
"name": "Системный промпт"
|
||||||
|
},
|
||||||
|
"response_time": {
|
||||||
|
"name": "Время последнего ответа"
|
||||||
|
},
|
||||||
|
"total_responses": {
|
||||||
|
"name": "Всего ответов"
|
||||||
|
},
|
||||||
|
"error_count": {
|
||||||
|
"name": "Количество ошибок"
|
||||||
|
},
|
||||||
|
"last_error": {
|
||||||
|
"name": "Последняя ошибка"
|
||||||
|
},
|
||||||
|
"api_status": {
|
||||||
|
"name": "Статус API"
|
||||||
|
},
|
||||||
|
"tokens_used": {
|
||||||
|
"name": "Всего использовано токенов"
|
||||||
|
},
|
||||||
|
"average_response_time": {
|
||||||
|
"name": "Среднее время ответа"
|
||||||
|
},
|
||||||
|
"last_request_time": {
|
||||||
|
"name": "Время последнего запроса"
|
||||||
|
},
|
||||||
|
"is_processing": {
|
||||||
|
"name": "Статус обработки"
|
||||||
|
},
|
||||||
|
"is_rate_limited": {
|
||||||
|
"name": "Статус лимита запросов"
|
||||||
|
},
|
||||||
|
"is_maintenance": {
|
||||||
|
"name": "Статус обслуживания"
|
||||||
|
},
|
||||||
|
"api_version": {
|
||||||
|
"name": "Версия API"
|
||||||
|
},
|
||||||
|
"endpoint_status": {
|
||||||
|
"name": "Статус эндпоинта"
|
||||||
|
},
|
||||||
|
"performance_metrics": {
|
||||||
|
"name": "Показатели производительности"
|
||||||
|
},
|
||||||
|
"history_size": {
|
||||||
|
"name": "Размер истории"
|
||||||
|
},
|
||||||
|
"uptime": {
|
||||||
|
"name": "Время работы"
|
||||||
|
},
|
||||||
|
"total_tokens": {
|
||||||
|
"name": "Всего токенов"
|
||||||
|
},
|
||||||
|
"prompt_tokens": {
|
||||||
|
"name": "Токены промпта"
|
||||||
|
},
|
||||||
|
"completion_tokens": {
|
||||||
|
"name": "Токены завершения"
|
||||||
|
},
|
||||||
|
"successful_requests": {
|
||||||
|
"name": "Успешные запросы"
|
||||||
|
},
|
||||||
|
"failed_requests": {
|
||||||
|
"name": "Неудачные запросы"
|
||||||
|
},
|
||||||
|
"average_latency": {
|
||||||
|
"name": "Средняя задержка"
|
||||||
|
},
|
||||||
|
"max_latency": {
|
||||||
|
"name": "Максимальная задержка"
|
||||||
|
},
|
||||||
|
"min_latency": {
|
||||||
|
"name": "Минимальная задержка"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "HA text AI",
|
"name": "HA text AI",
|
||||||
"render_readme": true,
|
"render_readme": true,
|
||||||
|
"icon": "mdi:robot",
|
||||||
"domains": ["sensor"],
|
"domains": ["sensor"],
|
||||||
"homeassistant": "2024.11.0",
|
"homeassistant": "2024.11.0",
|
||||||
"icon": "mdi:brain",
|
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"documentation": "https://github.com/smkrv/ha-text-ai"
|
"documentation": "https://github.com/smkrv/ha-text-ai"
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 618 KiB |
@@ -1,3 +0,0 @@
|
|||||||
pytest
|
|
||||||
pytest-asyncio
|
|
||||||
homeassistant
|
|
||||||
+3
-1
@@ -9,9 +9,11 @@ ha-text-ai/
|
|||||||
│ ├── manifest.json
|
│ ├── manifest.json
|
||||||
│ ├── sensor.py
|
│ ├── sensor.py
|
||||||
│ ├── services.yaml
|
│ ├── services.yaml
|
||||||
│ └── const.py
|
│ ├── const.py
|
||||||
|
│ └── api_client.py
|
||||||
│
|
│
|
||||||
└── strings/
|
└── strings/
|
||||||
├── en.json
|
├── en.json
|
||||||
|
├── de.json
|
||||||
└── ru.json
|
└── ru.json
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user