mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-22 07:03:58 +08:00
Compare commits
221
Commits
v1.0.3
...
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 | ||
|
|
5037622fb6 | ||
|
|
962a089bf4 | ||
|
|
4c56565b66 | ||
|
|
8432038f09 | ||
|
|
929d916d41 | ||
|
|
675975d951 | ||
|
|
524ec87395 | ||
|
|
94f8193996 | ||
|
|
7c3fcf73c3 | ||
|
|
20bbf89679 | ||
|
|
86dca52d07 | ||
|
|
78d1561e74 | ||
|
|
054e4af258 | ||
|
|
dc03faa97e | ||
|
|
9a7635c2ae | ||
|
|
df3d79c20c | ||
|
|
d6144be7ed | ||
|
|
9afbb904b3 | ||
|
|
93558b2444 | ||
|
|
9f93f1ee18 | ||
|
|
30a9b53ba1 | ||
|
|
5175970d55 | ||
|
|
f6bfbd4a07 | ||
|
|
4ddb0dc977 | ||
|
|
24dc4ac4d4 | ||
|
|
976f4f16a3 | ||
|
|
2bdef2b494 | ||
|
|
42324a793b | ||
|
|
30aa894634 | ||
|
|
398b2550a9 | ||
|
|
f1deaa2014 | ||
|
|
f4b0ce902e |
+35
-4
@@ -1,8 +1,39 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$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
|
||||
.env
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
Thumbs.db
|
||||
*.psd
|
||||
*.zip
|
||||
|
||||
+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,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
@@ -1,160 +1,236 @@
|
||||
# 🤖 HA Text AI for Home Assistant
|
||||
|
||||
<div align="center">
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://opensource.org/licenses/MIT)
|
||||
    [](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>
|
||||
|
||||
<p align="center">
|
||||
Transform your smart home experience with powerful AI assistance powered by OpenAI's GPT models
|
||||
Transform your smart home experience with powerful AI assistance powered by multiple AI providers including OpenAI GPT and Anthropic Claude models. Get intelligent responses, automate complex scenarios, and enhance your home automation with advanced natural language processing.
|
||||
</p>
|
||||
|
||||
---
|
||||
> [!NOTE]
|
||||
> 🚧 ALPHA VERSION 🚧
|
||||
> Expect: potential bugs, frequent changes, incomplete features.
|
||||
> 🤝 Community Driven
|
||||
|
||||
## 🌟 Features
|
||||
|
||||
- 🧠 **Advanced AI Integration**: Leverage OpenAI's powerful models (GPT-3.5, GPT-4) for smart home interactions
|
||||
- 💬 **Natural Language Control**: Control your home and get information using everyday language
|
||||
- 📝 **Conversation Memory**: Maintain context with conversation history tracking
|
||||
- ⚡ **Real-time Responses**: Get quick, contextual responses to your queries
|
||||
- 🎯 **Customizable Behavior**: Fine-tune AI responses with adjustable parameters
|
||||
- 🔒 **Secure Integration**: Your API key and data are handled securely
|
||||
- 🎨 **Flexible Configuration**: Easy setup with multiple configuration options
|
||||
- 🔄 **Automation Ready**: Integrate AI responses into your automations
|
||||
- 🧠 **Multi-Provider AI Integration**:
|
||||
- Support for OpenAI GPT models
|
||||
- Anthropic Claude integration
|
||||
- Custom API endpoints
|
||||
- Flexible model selection
|
||||
|
||||
- 💬 **Advanced Language Processing**:
|
||||
- Context-aware responses
|
||||
- Multi-turn conversations
|
||||
- Custom system instructions
|
||||
- Natural conversation flow
|
||||
|
||||
- 📝 **Enhanced Memory Management**:
|
||||
- Persistent conversation history
|
||||
- Context-aware responses
|
||||
- Customizable history limits
|
||||
- Model-specific filtering
|
||||
|
||||
- ⚡ **Performance Optimization**:
|
||||
- Efficient token usage
|
||||
- Smart rate limiting
|
||||
- Response caching
|
||||
- Request interval control
|
||||
|
||||
- 🎯 **Advanced Customization**:
|
||||
- Per-request model selection
|
||||
- Adjustable parameters
|
||||
- Custom system prompts
|
||||
- Temperature control
|
||||
|
||||
- 🔒 **Enhanced Security**:
|
||||
- Secure API key storage
|
||||
- Rate limiting protection
|
||||
- Error handling
|
||||
- Usage monitoring
|
||||
|
||||
- 🎨 **Improved User Experience**:
|
||||
- Intuitive configuration UI
|
||||
- Detailed sensor attributes
|
||||
- Rich service interface
|
||||
- Model selection UI
|
||||
|
||||
- 🔄 **Automation Integration**:
|
||||
- Event-driven responses
|
||||
- Conditional logic support
|
||||
- Template compatibility
|
||||
- Model-specific automation
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
- Home Assistant installation (Core, OS, Container, or Supervised)
|
||||
- OpenAI API key ([Get one here](https://platform.openai.com/account/api-keys))
|
||||
- Home Assistant 2023.11 or later
|
||||
- Active API key from:
|
||||
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
|
||||
- Anthropic ([Get key](https://console.anthropic.com/))
|
||||
- OpenRouter ([Get key](https://openrouter.ai/keys))
|
||||
- Python 3.9 or newer
|
||||
- Stable internet connection
|
||||
|
||||
## ⚡ Quick Start
|
||||
### Configuration Options
|
||||
- API Provider (OpenAI/Anthropic)
|
||||
- API Key (provider-specific)
|
||||
- Model Selection (flexible, provider-specific models)
|
||||
- Temperature (Creativity control, 0.0-2.0)
|
||||
- Max Tokens (Response length limit)
|
||||
- Request Interval (API call throttling)
|
||||
- Custom API Endpoint (optional)
|
||||
|
||||
#### ⓘ Potentially Compatible Providers
|
||||
The integration is designed to be flexible and may work with other providers offering OpenAI-compatible APIs:
|
||||
- Groq
|
||||
- Together AI
|
||||
- Perplexity AI
|
||||
- Mistral AI
|
||||
- Google AI
|
||||
- Local AI servers (like Ollama)
|
||||
- Custom OpenAI-compatible endpoints
|
||||
|
||||
#### Additional Notes
|
||||
- Not all providers guarantee full compatibility
|
||||
- Performance may vary between providers
|
||||
- Check individual provider's documentation
|
||||
- Ensure your API key has sufficient credits/quota
|
||||
|
||||
#### Provider Compatibility Requirements
|
||||
To be compatible, a provider should support:
|
||||
- OpenAI-like REST API structure
|
||||
- JSON request/response format
|
||||
- Standard authentication method
|
||||
- Similar model parameter handling
|
||||
|
||||
## ⚡ Installation
|
||||
|
||||
### HACS Installation (Recommended)
|
||||
<a href="https://my.home-assistant.io/redirect/hacs_repository/?owner=smkrv&repository=ha-text-ai&category=Integration"><img src="https://my.home-assistant.io/badges/hacs_repository.svg" width="170" height="auto"></a>
|
||||
1. Open HACS in Home Assistant
|
||||
2. Click on "Integrations"
|
||||
3. Click "..." in top right corner
|
||||
4. Select "Custom repositories"
|
||||
5. Add repository URL: `https://github.com/smkrv/ha-text-ai`
|
||||
6. Choose "Integration" as category
|
||||
7. Click "Download"
|
||||
8. Restart Home Assistant
|
||||
|
||||
### Manual Installation
|
||||
1. Download the repository
|
||||
2. Copy `custom_components/ha_text_ai` to your `custom_components` directory
|
||||
1. Download the latest release
|
||||
2. Extract and copy `custom_components/ha_text_ai` to your `custom_components` directory
|
||||
3. Restart Home Assistant
|
||||
4. Add configuration to `configuration.yaml`:
|
||||
4. Add configuration via UI or YAML
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### Via UI (Recommended)
|
||||
1. Go to Settings → Devices & Services
|
||||
2. Click "Add Integration"
|
||||
3. Search for "HA Text AI"
|
||||
4. Follow the configuration steps
|
||||
|
||||
### Via YAML
|
||||
```yaml
|
||||
ha_text_ai:
|
||||
api_key: !secret openai_api_key
|
||||
api_provider: openai # or anthropic
|
||||
api_key: !secret ai_api_key
|
||||
model: gpt-4o-mini
|
||||
temperature: 0.7
|
||||
max_tokens: 1000
|
||||
request_interval: 1.0
|
||||
api_endpoint: https://api.openai.com/v1 # optional, for custom endpoints
|
||||
system_prompt: |
|
||||
You are a home automation expert assistant.
|
||||
Focus on practical and efficient solutions.
|
||||
```
|
||||
|
||||
## ⚙️ Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `api_key` | string | Required | Your OpenAI API key |
|
||||
| `model` | string | `gpt-3.5-turbo` | AI model to use |
|
||||
| `temperature` | float | `0.7` | Response creativity (0-2) |
|
||||
| `max_tokens` | integer | `1000` | Maximum response length |
|
||||
| `request_interval` | float | `1.0` | Minimum seconds between requests |
|
||||
| `api_endpoint` | string | OpenAI default | Custom API endpoint URL |
|
||||
|
||||
## 🛠️ Available Services
|
||||
|
||||
### ask_question
|
||||
Ask the AI assistant a question:
|
||||
```yaml
|
||||
service: ha_text_ai.ask_question
|
||||
data:
|
||||
question: "What's the optimal temperature for sleeping?"
|
||||
model: "gpt-4" # optional
|
||||
model: "claude-3-sonnet" # optional
|
||||
temperature: 0.5 # optional
|
||||
max_tokens: 500 # optional
|
||||
context_messages: 10 #optional, number of previous messages to include in context, default: 5
|
||||
system_prompt: "You are a sleep optimization expert" # optional
|
||||
```
|
||||
|
||||
### set_system_prompt
|
||||
Configure AI behavior:
|
||||
```yaml
|
||||
service: ha_text_ai.set_system_prompt
|
||||
data:
|
||||
prompt: "You are a home automation expert focused on energy efficiency"
|
||||
prompt: |
|
||||
You are a home automation expert focused on:
|
||||
1. Energy efficiency
|
||||
2. Comfort optimization
|
||||
3. Security considerations
|
||||
Provide practical, actionable advice.
|
||||
```
|
||||
|
||||
### clear_history
|
||||
Reset conversation history:
|
||||
```yaml
|
||||
service: ha_text_ai.clear_history
|
||||
```
|
||||
|
||||
### get_history
|
||||
Retrieve conversation history:
|
||||
```yaml
|
||||
service: ha_text_ai.get_history
|
||||
data:
|
||||
limit: 5 # optional
|
||||
filter_model: "gpt-4o" # optional
|
||||
```
|
||||
|
||||
## 🔧 Practical Examples
|
||||
## 📘 FAQ
|
||||
|
||||
### Smart Temperature Management
|
||||
```yaml
|
||||
automation:
|
||||
trigger:
|
||||
platform: time_pattern
|
||||
hours: "/1"
|
||||
action:
|
||||
service: ha_text_ai.ask_question
|
||||
data:
|
||||
question: >
|
||||
Current temperature is {{ states('sensor.living_room_temperature') }}°C.
|
||||
Should I adjust the thermostat for optimal comfort and energy savings?
|
||||
```
|
||||
**Q: Which AI providers are supported?**
|
||||
A: Currently OpenAI (GPT models) and Anthropic (Claude models) are supported, with more providers planned.
|
||||
|
||||
### Smart Lighting Assistant
|
||||
```yaml
|
||||
automation:
|
||||
trigger:
|
||||
platform: state
|
||||
entity_id: binary_sensor.living_room_motion
|
||||
to: 'on'
|
||||
condition:
|
||||
condition: template
|
||||
value_template: "{{ states('sensor.illuminance') | float < 10 }}"
|
||||
action:
|
||||
service: ha_text_ai.ask_question
|
||||
data:
|
||||
question: >
|
||||
Motion detected in living room with low light levels.
|
||||
What's the best lighting scene to set based on the time of day?
|
||||
```
|
||||
**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.
|
||||
|
||||
## ❗ Common Issues
|
||||
**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.
|
||||
|
||||
### API Rate Limits
|
||||
- Increase `request_interval` if hitting rate limits
|
||||
- Consider upgrading your OpenAI plan
|
||||
- Use caching for frequent queries
|
||||
**Q: Can I use custom models?**
|
||||
A: Yes, you can configure custom endpoints and use any compatible model by specifying it in the configuration.
|
||||
|
||||
### High Token Usage
|
||||
- Reduce `max_tokens` parameter
|
||||
- Clear conversation history regularly
|
||||
- Use focused system prompts
|
||||
**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.
|
||||
|
||||
### Connection Issues
|
||||
- Check internet connectivity
|
||||
- Verify API key validity
|
||||
- Ensure endpoint accessibility
|
||||
**Q: How can I reduce API costs?**
|
||||
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage.
|
||||
|
||||
**Q: Is my data secure?**
|
||||
A: Yes, your data is secure. The system operates entirely on your local machine, keeping your data under your control. API keys are stored securely and all external communications use encrypted connections.
|
||||
|
||||
**Q: How do context messages work?**
|
||||
A: Context messages allow the AI to remember and reference previous conversation history. By default, 5 previous messages are included, but you can customize this from 1 to 20 messages to control the conversation depth and token usage.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Open a Pull Request
|
||||
2. Create feature branch (`git checkout -b feature/Enhancement`)
|
||||
3. Commit changes (`git commit -m 'Add Enhancement'`)
|
||||
4. Push branch (`git push origin feature/Enhancement`)
|
||||
5. Open Pull Request
|
||||
|
||||
## 📝 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
@@ -162,4 +238,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
||||
|
||||
Made with ❤️ for the Home Assistant Community
|
||||
|
||||
[Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,209 +1,295 @@
|
||||
"""The HA text AI integration."""
|
||||
"""The HA Text AI integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
from async_timeout import timeout
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady
|
||||
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 .api_client import APIClient
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
SERVICE_ASK_QUESTION,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
SERVICE_GET_HISTORY,
|
||||
SERVICE_SET_SYSTEM_PROMPT,
|
||||
CONF_MODEL,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_API_ENDPOINT,
|
||||
CONF_REQUEST_INTERVAL,
|
||||
CONF_API_PROVIDER,
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_OPENAI_ENDPOINT,
|
||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
DEFAULT_CONTEXT_MESSAGES,
|
||||
API_TIMEOUT,
|
||||
SERVICE_ASK_QUESTION,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
SERVICE_GET_HISTORY,
|
||||
SERVICE_SET_SYSTEM_PROMPT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def get_coordinator():
|
||||
"""Get coordinator class with lazy import to avoid circular deps."""
|
||||
from .coordinator import HATextAICoordinator
|
||||
return HATextAICoordinator
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_MODEL, default="gpt-3.5-turbo"): cv.string,
|
||||
vol.Optional(CONF_TEMPERATURE, default=0.7): vol.Coerce(float),
|
||||
vol.Optional(CONF_MAX_TOKENS, default=1000): vol.Coerce(int),
|
||||
vol.Optional(CONF_REQUEST_INTERVAL, default=1.0): vol.Coerce(float),
|
||||
vol.Optional(CONF_API_ENDPOINT): cv.string,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||
vol.Required("instance"): cv.string,
|
||||
vol.Required("question"): cv.string,
|
||||
vol.Optional("system_prompt"): cv.string,
|
||||
vol.Optional("model"): cv.string,
|
||||
vol.Optional("temperature"): cv.positive_float,
|
||||
vol.Optional("max_tokens"): cv.positive_int,
|
||||
vol.Optional("context_messages"): cv.positive_int,
|
||||
})
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
|
||||
"""Set up the HA text AI component."""
|
||||
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||
vol.Required("instance"): cv.string,
|
||||
vol.Required("prompt"): cv.string,
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
||||
vol.Required("instance"): cv.string,
|
||||
vol.Optional("limit"): cv.positive_int,
|
||||
vol.Optional("filter_model"): cv.string,
|
||||
})
|
||||
|
||||
def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator:
|
||||
"""Get coordinator by instance name."""
|
||||
if instance.startswith("sensor."):
|
||||
instance = instance.replace("sensor.ha_text_ai_", "", 1)
|
||||
|
||||
for entry_id, coord in hass.data[DOMAIN].items():
|
||||
if isinstance(coord, HATextAICoordinator) and coord.instance_name.lower() == instance.lower():
|
||||
return coord
|
||||
|
||||
raise HomeAssistantError(f"Instance {instance} not found")
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
||||
"""Set up the HA Text AI component."""
|
||||
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 the ask_question service call."""
|
||||
if not hass.data[DOMAIN]:
|
||||
raise HomeAssistantError("No AI Text integration configured")
|
||||
|
||||
coordinator = next(iter(hass.data[DOMAIN].values()))
|
||||
question = call.data["question"]
|
||||
|
||||
original_params = {
|
||||
"model": coordinator.model,
|
||||
"temperature": coordinator.temperature,
|
||||
"max_tokens": coordinator.max_tokens
|
||||
}
|
||||
|
||||
"""Handle ask_question service."""
|
||||
try:
|
||||
if "model" in call.data:
|
||||
coordinator.model = call.data["model"]
|
||||
if "temperature" in call.data:
|
||||
coordinator.temperature = call.data["temperature"]
|
||||
if "max_tokens" in call.data:
|
||||
coordinator.max_tokens = call.data["max_tokens"]
|
||||
|
||||
await coordinator.async_ask_question(question)
|
||||
except Exception as ex:
|
||||
_LOGGER.error("Error asking question: %s", str(ex))
|
||||
raise HomeAssistantError(f"Failed to ask question: {str(ex)}") from ex
|
||||
finally:
|
||||
coordinator.model = original_params["model"]
|
||||
coordinator.temperature = original_params["temperature"]
|
||||
coordinator.max_tokens = original_params["max_tokens"]
|
||||
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 the clear_history service call."""
|
||||
if not hass.data[DOMAIN]:
|
||||
raise HomeAssistantError("No AI Text integration configured")
|
||||
"""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)}")
|
||||
|
||||
coordinator = next(iter(hass.data[DOMAIN].values()))
|
||||
coordinator._responses.clear()
|
||||
await coordinator.async_refresh()
|
||||
|
||||
async def async_get_history(call: ServiceCall) -> dict[str, list]:
|
||||
"""Handle the get_history service call."""
|
||||
if not hass.data[DOMAIN]:
|
||||
raise HomeAssistantError("No AI Text integration configured")
|
||||
|
||||
coordinator = next(iter(hass.data[DOMAIN].values()))
|
||||
if not coordinator._responses:
|
||||
return {"history": []}
|
||||
|
||||
limit = call.data.get("limit", 10)
|
||||
history = list(coordinator._responses.items())
|
||||
limited_history = history[-limit:] if len(history) > limit else history
|
||||
|
||||
return {
|
||||
"history": [
|
||||
{"question": q, "response": r} for q, r in limited_history
|
||||
]
|
||||
}
|
||||
async def async_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 the set_system_prompt service call."""
|
||||
if not hass.data[DOMAIN]:
|
||||
raise HomeAssistantError("No AI Text integration configured")
|
||||
|
||||
coordinator = next(iter(hass.data[DOMAIN].values()))
|
||||
coordinator.system_prompt = call.data["prompt"]
|
||||
"""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=vol.Schema({
|
||||
vol.Required("question"): cv.string,
|
||||
vol.Optional("model"): cv.string,
|
||||
vol.Optional("temperature"): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0, max=2)
|
||||
),
|
||||
vol.Optional("max_tokens"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=4096)
|
||||
),
|
||||
})
|
||||
schema=SERVICE_SCHEMA_ASK_QUESTION
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
async_clear_history,
|
||||
schema=vol.Schema({})
|
||||
schema=vol.Schema({vol.Required("instance"): cv.string})
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_HISTORY,
|
||||
async_get_history,
|
||||
schema=vol.Schema({
|
||||
vol.Optional("limit", default=10): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1)
|
||||
),
|
||||
})
|
||||
schema=SERVICE_SCHEMA_GET_HISTORY
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_SYSTEM_PROMPT,
|
||||
async_set_system_prompt,
|
||||
schema=vol.Schema({
|
||||
vol.Required("prompt"): cv.string,
|
||||
})
|
||||
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up HA text AI from a config entry."""
|
||||
HATextAICoordinator = get_coordinator()
|
||||
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
|
||||
"""Check API availability for different providers."""
|
||||
try:
|
||||
coordinator = HATextAICoordinator(
|
||||
hass,
|
||||
api_key=entry.data[CONF_API_KEY],
|
||||
endpoint=entry.data.get(CONF_API_ENDPOINT),
|
||||
model=entry.data.get(CONF_MODEL),
|
||||
temperature=entry.data.get(CONF_TEMPERATURE),
|
||||
max_tokens=entry.data.get(CONF_MAX_TOKENS),
|
||||
request_interval=entry.data.get(CONF_REQUEST_INTERVAL),
|
||||
if provider == API_PROVIDER_ANTHROPIC:
|
||||
check_url = f"{endpoint}/v1/models"
|
||||
else: # OpenAI
|
||||
check_url = f"{endpoint}/models"
|
||||
|
||||
async with timeout(API_TIMEOUT):
|
||||
async with session.get(check_url, headers=headers) as response:
|
||||
if response.status in [200, 404]:
|
||||
return True
|
||||
elif response.status == 401:
|
||||
raise ConfigEntryNotReady("Invalid API key")
|
||||
elif response.status == 429:
|
||||
_LOGGER.warning("Rate limit exceeded during API check")
|
||||
return False
|
||||
else:
|
||||
_LOGGER.error("API check failed with status: %d", response.status)
|
||||
return False
|
||||
except Exception as ex:
|
||||
_LOGGER.error("API check error: %s", str(ex))
|
||||
return False
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up HA Text AI from a config entry."""
|
||||
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)
|
||||
api_provider = entry.data.get(CONF_API_PROVIDER)
|
||||
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
|
||||
endpoint = entry.data.get(
|
||||
CONF_API_ENDPOINT,
|
||||
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
|
||||
else DEFAULT_ANTHROPIC_ENDPOINT
|
||||
).rstrip('/')
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
|
||||
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
if is_anthropic:
|
||||
headers["x-api-key"] = api_key
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
else:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
if not await async_check_api(session, endpoint, headers, api_provider):
|
||||
raise ConfigEntryNotReady("API connection failed")
|
||||
|
||||
_LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint)
|
||||
|
||||
api_client = APIClient(
|
||||
session=session,
|
||||
endpoint=endpoint,
|
||||
headers=headers,
|
||||
api_provider=api_provider,
|
||||
model=model,
|
||||
)
|
||||
|
||||
coordinator = HATextAICoordinator(
|
||||
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()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
return await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
_LOGGER.info(
|
||||
"Successfully set up %s instance '%s' with model %s",
|
||||
api_provider,
|
||||
instance_name,
|
||||
model
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as ex:
|
||||
raise ConfigEntryNotReady(f"Failed to setup entry: {str(ex)}") from ex
|
||||
_LOGGER.exception("Setup error: %s", str(ex))
|
||||
raise ConfigEntryNotReady(f"Setup error: {str(ex)}") from ex
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if entry.entry_id not in hass.data.get(DOMAIN, {}):
|
||||
return True
|
||||
try:
|
||||
if entry.entry_id in hass.data[DOMAIN]:
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if hasattr(coordinator.client, 'shutdown'):
|
||||
await coordinator.client.shutdown()
|
||||
|
||||
# Remove services only if this was the last instance
|
||||
if not hass.data[DOMAIN]:
|
||||
services = [
|
||||
SERVICE_ASK_QUESTION,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
SERVICE_GET_HISTORY,
|
||||
SERVICE_SET_SYSTEM_PROMPT
|
||||
]
|
||||
for service in services:
|
||||
if DOMAIN in hass.services.async_services() and \
|
||||
service in hass.services.async_services()[DOMAIN]:
|
||||
hass.services.async_remove(DOMAIN, service)
|
||||
await coordinator.async_shutdown()
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
except Exception as ex:
|
||||
_LOGGER.exception("Error unloading entry: %s", str(ex))
|
||||
return False
|
||||
|
||||
@@ -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,12 +1,14 @@
|
||||
"""Config flow for HA text AI integration."""
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
import voluptuous as vol
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
import openai
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import selector
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
@@ -15,142 +17,231 @@ from .const import (
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_API_ENDPOINT,
|
||||
CONF_REQUEST_INTERVAL,
|
||||
CONF_API_PROVIDER,
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
API_PROVIDERS,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_API_ENDPOINT,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
DEFAULT_OPENAI_ENDPOINT,
|
||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||
DEFAULT_CONTEXT_MESSAGES,
|
||||
MIN_TEMPERATURE,
|
||||
MAX_TEMPERATURE,
|
||||
MIN_MAX_TOKENS,
|
||||
MAX_MAX_TOKENS,
|
||||
MIN_REQUEST_INTERVAL,
|
||||
)
|
||||
|
||||
import logging
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str,
|
||||
vol.Optional(
|
||||
CONF_TEMPERATURE,
|
||||
default=DEFAULT_TEMPERATURE
|
||||
): vol.All(vol.Coerce(float), vol.Range(min=0, max=2)),
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
default=DEFAULT_MAX_TOKENS
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=1, max=4096)),
|
||||
vol.Optional(CONF_API_ENDPOINT, default=DEFAULT_API_ENDPOINT): str,
|
||||
vol.Optional(
|
||||
CONF_REQUEST_INTERVAL,
|
||||
default=DEFAULT_REQUEST_INTERVAL
|
||||
): vol.All(vol.Coerce(float), vol.Range(min=0.1)),
|
||||
})
|
||||
|
||||
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for HA text AI."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self,
|
||||
user_input: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
def __init__(self) -> None:
|
||||
"""Initialize flow."""
|
||||
self._errors = {}
|
||||
self._data = {}
|
||||
self._provider = None
|
||||
|
||||
async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: Dict[str, str] = {}
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_API_PROVIDER): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=API_PROVIDERS,
|
||||
translation_key="api_provider"
|
||||
)
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
# Create OpenAI client
|
||||
client = openai.OpenAI(
|
||||
api_key=user_input[CONF_API_KEY],
|
||||
base_url=user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT)
|
||||
)
|
||||
self._provider = user_input[CONF_API_PROVIDER]
|
||||
return await self.async_step_provider()
|
||||
|
||||
# Verify API connection and model availability
|
||||
models = await self.hass.async_add_executor_job(client.models.list)
|
||||
model_ids = [model.id for model in models.data]
|
||||
async def async_step_provider(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||
"""Handle provider configuration step."""
|
||||
if user_input is None:
|
||||
default_endpoint = (
|
||||
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
|
||||
else DEFAULT_ANTHROPIC_ENDPOINT
|
||||
)
|
||||
|
||||
if user_input[CONF_MODEL] not in model_ids:
|
||||
_LOGGER.warning(
|
||||
"Selected model %s not found in available models: %s",
|
||||
user_input[CONF_MODEL],
|
||||
", ".join(model_ids)
|
||||
)
|
||||
errors["base"] = "invalid_model"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_API_KEY])
|
||||
self._abort_if_unique_id_configured()
|
||||
suggested_name = f"HA Text AI {len(self._async_current_entries()) + 1}"
|
||||
|
||||
return self.async_create_entry(
|
||||
title="HA text AI",
|
||||
data=user_input
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="provider",
|
||||
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
|
||||
)
|
||||
|
||||
except openai.AuthenticationError as err:
|
||||
_LOGGER.error("Authentication failed: %s", str(err))
|
||||
errors["base"] = "invalid_auth"
|
||||
except openai.APIError as err:
|
||||
_LOGGER.error("API connection failed: %s", str(err))
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected error: %s", str(err))
|
||||
errors["base"] = "unknown"
|
||||
instance_name = user_input[CONF_NAME]
|
||||
await self._async_validate_name(instance_name)
|
||||
if self._errors:
|
||||
return await self.async_step_provider()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"default_model": DEFAULT_MODEL,
|
||||
"default_endpoint": DEFAULT_API_ENDPOINT,
|
||||
if not await self._async_validate_api(user_input):
|
||||
return await self.async_step_provider()
|
||||
|
||||
return await self._create_entry(user_input)
|
||||
|
||||
async def _async_validate_name(self, name: str) -> bool:
|
||||
"""Validate that the name is unique."""
|
||||
for entry in self._async_current_entries():
|
||||
if entry.data.get(CONF_NAME) == name:
|
||||
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
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> config_entries.OptionsFlow:
|
||||
def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle options flow for HA text AI."""
|
||||
"""Handle options flow."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self,
|
||||
user_input: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle options flow."""
|
||||
async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
options_schema = vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_TEMPERATURE,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_TEMPERATURE, DEFAULT_TEMPERATURE
|
||||
),
|
||||
description={"suggested_value": DEFAULT_TEMPERATURE},
|
||||
): vol.All(vol.Coerce(float), vol.Range(min=0, max=2)),
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS
|
||||
),
|
||||
description={"suggested_value": DEFAULT_MAX_TOKENS},
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=1, max=4096)),
|
||||
vol.Optional(
|
||||
CONF_REQUEST_INTERVAL,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
|
||||
),
|
||||
description={"suggested_value": DEFAULT_REQUEST_INTERVAL},
|
||||
): vol.All(vol.Coerce(float), vol.Range(min=0.1)),
|
||||
})
|
||||
current_data = {**self.config_entry.data, **self.config_entry.options}
|
||||
|
||||
return self.async_show_form(
|
||||
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,24 +1,47 @@
|
||||
"""Constants for the HA text AI integration."""
|
||||
from typing import Final
|
||||
from homeassistant.const import Platform
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
# Domain
|
||||
# Domain and platforms
|
||||
DOMAIN: Final = "ha_text_ai"
|
||||
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
|
||||
CONF_MODEL: Final = "model"
|
||||
CONF_TEMPERATURE: Final = "temperature"
|
||||
CONF_MAX_TOKENS: Final = "max_tokens"
|
||||
CONF_API_ENDPOINT: Final = "api_endpoint"
|
||||
CONF_REQUEST_INTERVAL: Final = "request_interval"
|
||||
CONF_INSTANCE: Final = "instance"
|
||||
CONF_MAX_HISTORY_SIZE: Final = "max_history_size"
|
||||
CONF_IS_ANTHROPIC: Final = "is_anthropic"
|
||||
CONF_CONTEXT_MESSAGES: Final = "context_messages"
|
||||
|
||||
# Default values
|
||||
DEFAULT_MODEL: Final = "gpt-3.5-turbo"
|
||||
DEFAULT_TEMPERATURE: Final = 0.7
|
||||
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
||||
DEFAULT_TEMPERATURE: Final = 0.1
|
||||
DEFAULT_MAX_TOKENS: Final = 1000
|
||||
DEFAULT_API_ENDPOINT: Final = "https://api.openai.com/v1"
|
||||
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
||||
DEFAULT_TIMEOUT: Final = 30
|
||||
DEFAULT_MAX_HISTORY: Final = 50
|
||||
DEFAULT_NAME: Final = "HA Text AI"
|
||||
DEFAULT_CONTEXT_MESSAGES: Final = 5
|
||||
|
||||
# Parameter constraints
|
||||
MIN_TEMPERATURE: Final = 0.0
|
||||
@@ -26,6 +49,11 @@ MAX_TEMPERATURE: Final = 2.0
|
||||
MIN_MAX_TOKENS: Final = 1
|
||||
MAX_MAX_TOKENS: Final = 4096
|
||||
MIN_REQUEST_INTERVAL: Final = 0.1
|
||||
MAX_REQUEST_INTERVAL: Final = 60.0
|
||||
|
||||
# API constants
|
||||
API_TIMEOUT: Final = 30
|
||||
API_RETRY_COUNT: Final = 3
|
||||
|
||||
# Service names
|
||||
SERVICE_ASK_QUESTION: Final = "ask_question"
|
||||
@@ -33,22 +61,48 @@ SERVICE_CLEAR_HISTORY: Final = "clear_history"
|
||||
SERVICE_GET_HISTORY: Final = "get_history"
|
||||
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
|
||||
ATTR_QUESTION: Final = "question"
|
||||
ATTR_RESPONSE: Final = "response"
|
||||
ATTR_LAST_UPDATED: Final = "last_updated"
|
||||
ATTR_INSTANCE: Final = "instance"
|
||||
ATTR_MODEL: Final = "model"
|
||||
ATTR_TEMPERATURE: Final = "temperature"
|
||||
ATTR_MAX_TOKENS: Final = "max_tokens"
|
||||
ATTR_TOTAL_RESPONSES: Final = "total_responses"
|
||||
ATTR_SYSTEM_PROMPT: Final = "system_prompt"
|
||||
ATTR_RESPONSE_TIME: Final = "response_time"
|
||||
ATTR_API_STATUS: Final = "api_status"
|
||||
ATTR_ERROR_COUNT: Final = "error_count"
|
||||
ATTR_CONVERSATION_HISTORY: Final = "conversation_history"
|
||||
|
||||
# Sensor attributes
|
||||
ATTR_TOTAL_RESPONSES: Final = "total_responses"
|
||||
ATTR_TOTAL_ERRORS: Final = "total_errors"
|
||||
ATTR_AVG_RESPONSE_TIME: Final = "average_response_time"
|
||||
ATTR_LAST_REQUEST_TIME: Final = "last_request_time"
|
||||
ATTR_LAST_ERROR: Final = "last_error"
|
||||
ATTR_IS_PROCESSING: Final = "is_processing"
|
||||
ATTR_IS_RATE_LIMITED: Final = "is_rate_limited"
|
||||
ATTR_IS_MAINTENANCE: Final = "is_maintenance"
|
||||
ATTR_API_VERSION: Final = "api_version"
|
||||
ATTR_ENDPOINT_STATUS: Final = "endpoint_status"
|
||||
ATTR_PERFORMANCE_METRICS: Final = "performance_metrics"
|
||||
ATTR_HISTORY_SIZE: Final = "history_size"
|
||||
ATTR_UPTIME: Final = "uptime"
|
||||
ATTR_API_PROVIDER: Final = "api_provider"
|
||||
ATTR_METRICS: Final = "metrics"
|
||||
ATTR_STATE: Final = "state"
|
||||
ATTR_LAST_RESPONSE: Final = "last_response"
|
||||
ATTR_ERROR: Final = "error"
|
||||
ATTR_TIMESTAMP: Final = "timestamp"
|
||||
|
||||
# Sensor metrics
|
||||
METRIC_TOTAL_TOKENS: Final = "total_tokens"
|
||||
METRIC_PROMPT_TOKENS: Final = "prompt_tokens"
|
||||
METRIC_COMPLETION_TOKENS: Final = "completion_tokens"
|
||||
METRIC_SUCCESSFUL_REQUESTS: Final = "successful_requests"
|
||||
METRIC_FAILED_REQUESTS: Final = "failed_requests"
|
||||
METRIC_AVERAGE_LATENCY: Final = "average_latency"
|
||||
METRIC_MAX_LATENCY: Final = "max_latency"
|
||||
METRIC_MIN_LATENCY: Final = "min_latency"
|
||||
|
||||
# Error messages
|
||||
ERROR_INVALID_API_KEY: Final = "invalid_api_key"
|
||||
@@ -58,35 +112,90 @@ ERROR_INVALID_MODEL: Final = "invalid_model"
|
||||
ERROR_RATE_LIMIT: Final = "rate_limit_exceeded"
|
||||
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
|
||||
ERROR_API_ERROR: Final = "api_error"
|
||||
|
||||
# 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"
|
||||
ERROR_TIMEOUT: Final = "timeout_error"
|
||||
ERROR_INVALID_INSTANCE: Final = "invalid_instance"
|
||||
ERROR_NAME_EXISTS: Final = "name_exists"
|
||||
|
||||
# Entity attributes
|
||||
ENTITY_NAME: Final = "HA Text AI"
|
||||
ENTITY_ICON: Final = "mdi:robot"
|
||||
|
||||
# Translation keys
|
||||
TRANSLATION_KEY_CONFIG: Final = "config"
|
||||
TRANSLATION_KEY_OPTIONS: Final = "options"
|
||||
TRANSLATION_KEY_ERROR: Final = "error"
|
||||
ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
|
||||
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
|
||||
|
||||
# State attributes
|
||||
STATE_READY: Final = "ready"
|
||||
STATE_PROCESSING: Final = "processing"
|
||||
STATE_ERROR: Final = "error"
|
||||
STATE_INITIALIZING: Final = "initializing"
|
||||
STATE_MAINTENANCE: Final = "maintenance"
|
||||
STATE_RATE_LIMITED: Final = "rate_limited"
|
||||
STATE_DISCONNECTED: Final = "disconnected"
|
||||
|
||||
# Event names
|
||||
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
|
||||
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
|
||||
EVENT_STATE_CHANGED: Final = f"{DOMAIN}_state_changed"
|
||||
|
||||
# Service schema constants
|
||||
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||
vol.Required(CONF_INSTANCE): cv.string,
|
||||
vol.Required("question"): cv.string,
|
||||
vol.Optional("system_prompt"): cv.string,
|
||||
vol.Optional("model"): cv.string,
|
||||
vol.Optional("temperature"): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||
),
|
||||
vol.Optional("max_tokens"): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||
),
|
||||
vol.Optional("context_messages"): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=20)
|
||||
)
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||
vol.Required(CONF_INSTANCE): cv.string,
|
||||
vol.Required("prompt"): cv.string
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
||||
vol.Required(CONF_INSTANCE): cv.string,
|
||||
vol.Optional("limit", default=10): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=100)
|
||||
),
|
||||
vol.Optional("filter_model"): cv.string
|
||||
})
|
||||
|
||||
# Configuration schema
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_API_PROVIDER): vol.In(API_PROVIDERS),
|
||||
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string,
|
||||
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||
),
|
||||
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||
),
|
||||
vol.Optional(CONF_API_ENDPOINT): cv.string,
|
||||
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
|
||||
),
|
||||
vol.Optional(CONF_MAX_HISTORY_SIZE, default=DEFAULT_MAX_HISTORY): vol.All(
|
||||
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,133 +1,377 @@
|
||||
"""The HA Text AI integration."""
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
"""The HA Text AI coordinator."""
|
||||
from __future__ import annotations
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up HA Text AI from a config entry."""
|
||||
try:
|
||||
coordinator = HATextAICoordinator(
|
||||
hass,
|
||||
api_key=entry.data["api_key"],
|
||||
endpoint=entry.data.get("api_endpoint", "https://api.openai.com/v1"),
|
||||
model=entry.data.get("model", "gpt-3.5-turbo"),
|
||||
temperature=entry.data.get("temperature", 0.7),
|
||||
max_tokens=entry.data.get("max_tokens", 1000),
|
||||
request_interval=entry.data.get("request_interval", 1.0),
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
return await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
except Exception as ex:
|
||||
raise ConfigEntryNotReady(f"Failed to setup entry: {str(ex)}") from ex
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
"""Data coordinator for HA text AI."""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import openai
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
STATE_READY,
|
||||
STATE_PROCESSING,
|
||||
STATE_ERROR,
|
||||
STATE_RATE_LIMITED,
|
||||
STATE_MAINTENANCE,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_HISTORY,
|
||||
DEFAULT_CONTEXT_MESSAGES,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class HATextAICoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching data from the API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
api_key: str,
|
||||
endpoint: str,
|
||||
client: Any,
|
||||
model: str,
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
request_interval: float,
|
||||
update_interval: int,
|
||||
instance_name: str,
|
||||
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||
temperature: float = DEFAULT_TEMPERATURE,
|
||||
max_history_size: int = DEFAULT_MAX_HISTORY,
|
||||
context_messages: int = DEFAULT_CONTEXT_MESSAGES,
|
||||
is_anthropic: bool = False,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
"""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)
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=request_interval),
|
||||
name=instance_name,
|
||||
update_interval=update_interval_td,
|
||||
)
|
||||
|
||||
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")
|
||||
# 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.endpoint = endpoint or "https://api.openai.com/v1"
|
||||
self.model = model or "gpt-3.5-turbo"
|
||||
self.temperature = float(temperature)
|
||||
self.max_tokens = int(max_tokens)
|
||||
self._question_queue = asyncio.Queue()
|
||||
self._responses: Dict[str, Any] = {}
|
||||
self.system_prompt: Optional[str] = None
|
||||
self._system_prompt = None
|
||||
self._conversation_history = []
|
||||
self._performance_metrics = self._initial_state["metrics"].copy()
|
||||
self._is_processing = False
|
||||
self._is_rate_limited = False
|
||||
self._is_maintenance = False
|
||||
self.endpoint_status = "ready"
|
||||
self.last_response = self._initial_state["last_response"].copy()
|
||||
self._start_time = dt_util.utcnow()
|
||||
|
||||
self.client = openai.OpenAI(
|
||||
api_key=self.api_key,
|
||||
base_url=self.endpoint
|
||||
)
|
||||
_LOGGER.info(f"Initialized HA Text AI coordinator with instance: {instance_name}")
|
||||
|
||||
async def _async_update_data(self) -> Dict[str, Any]:
|
||||
"""Update data via OpenAI API."""
|
||||
if self._question_queue.empty():
|
||||
return self._responses
|
||||
|
||||
"""Update data via library."""
|
||||
try:
|
||||
question = await self._question_queue.get()
|
||||
response_content = await self.hass.async_add_executor_job(
|
||||
self._make_api_call, question
|
||||
)
|
||||
self._responses[question] = {
|
||||
"question": question,
|
||||
"response": response_content
|
||||
current_state = self._get_current_state()
|
||||
_LOGGER.debug(f"Updating data for {self.instance_name}, current state: {current_state}")
|
||||
|
||||
data = {
|
||||
"state": current_state,
|
||||
"metrics": self._performance_metrics,
|
||||
"last_response": self.last_response,
|
||||
"is_processing": self._is_processing,
|
||||
"is_rate_limited": self._is_rate_limited,
|
||||
"is_maintenance": self._is_maintenance,
|
||||
"endpoint_status": self.endpoint_status,
|
||||
"uptime": (dt_util.utcnow() - self._start_time).total_seconds(),
|
||||
"system_prompt": self._system_prompt,
|
||||
"history_size": len(self._conversation_history),
|
||||
"conversation_history": self._conversation_history,
|
||||
}
|
||||
_LOGGER.debug("Response from API: %s", response_content)
|
||||
return self._responses
|
||||
|
||||
except openai.AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
# 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:
|
||||
_LOGGER.error("Error communicating with API: %s", err)
|
||||
return self._responses
|
||||
_LOGGER.error(f"Error updating data for {self.instance_name}: {err}")
|
||||
return self._initial_state
|
||||
|
||||
def _make_api_call(self, question: str) -> str:
|
||||
"""Make API call to OpenAI."""
|
||||
async def async_update_ha_state(self) -> None:
|
||||
"""Update Home Assistant state."""
|
||||
try:
|
||||
_LOGGER.debug(f"Requesting state update for {self.instance_name}")
|
||||
await self.async_request_refresh()
|
||||
|
||||
# Force update of all entities
|
||||
for entity_id in self.hass.states.async_entity_ids():
|
||||
if entity_id.startswith(f"sensor.ha_text_ai_{self.instance_name}"):
|
||||
self.hass.states.async_set(entity_id, self._get_current_state())
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.error(f"Error updating HA state for {self.instance_name}: {err}")
|
||||
|
||||
def _get_current_state(self) -> str:
|
||||
"""Get current state based on internal flags."""
|
||||
if self._is_processing:
|
||||
return STATE_PROCESSING
|
||||
elif self._is_rate_limited:
|
||||
return STATE_RATE_LIMITED
|
||||
elif self._is_maintenance:
|
||||
return STATE_MAINTENANCE
|
||||
elif self.last_response.get("error"):
|
||||
return STATE_ERROR
|
||||
return STATE_READY
|
||||
|
||||
def _calculate_context_tokens(self, messages: List[Dict[str, str]]) -> int:
|
||||
try:
|
||||
if self.is_anthropic and hasattr(self.client, 'count_tokens'):
|
||||
return sum(self.client.count_tokens(msg['content']) for msg in messages)
|
||||
|
||||
return sum(len(msg['content']) // 4 for msg in messages)
|
||||
except Exception as e:
|
||||
_LOGGER.warning(f"Error calculating context tokens: {e}")
|
||||
return 0
|
||||
|
||||
async def async_ask_question(
|
||||
self,
|
||||
question: str,
|
||||
model: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
context_messages: Optional[int] = None,
|
||||
) -> dict:
|
||||
"""Process a question with optional parameters."""
|
||||
return await self.async_process_question(
|
||||
question, model, temperature, max_tokens, system_prompt, context_messages
|
||||
)
|
||||
|
||||
async def async_process_question(
|
||||
self,
|
||||
question: str,
|
||||
model: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
context_messages: Optional[int] = None,
|
||||
) -> dict:
|
||||
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 self.system_prompt:
|
||||
messages.append({"role": "system", "content": self.system_prompt})
|
||||
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})
|
||||
|
||||
completion = self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
temperature=self.temperature,
|
||||
max_tokens=self.max_tokens,
|
||||
)
|
||||
return completion.choices[0].message.content
|
||||
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:
|
||||
_LOGGER.error("Error in API call: %s", 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
|
||||
}
|
||||
}
|
||||
|
||||
async def _process_openai_message(self, question: str, **kwargs) -> dict:
|
||||
"""Process message using OpenAI API."""
|
||||
try:
|
||||
response = await self.client.create(
|
||||
model=kwargs["model"],
|
||||
messages=kwargs["messages"],
|
||||
temperature=kwargs["temperature"],
|
||||
max_tokens=kwargs["max_tokens"],
|
||||
)
|
||||
|
||||
return {
|
||||
"content": response["choices"][0]["message"]["content"],
|
||||
"tokens": {
|
||||
"prompt": response["usage"]["prompt_tokens"],
|
||||
"completion": response["usage"]["completion_tokens"],
|
||||
"total": response["usage"]["total_tokens"]
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error in OpenAI API call: {str(e)}")
|
||||
raise
|
||||
|
||||
def _update_metrics(self, latency: float, response: dict) -> None:
|
||||
"""Update performance metrics."""
|
||||
metrics = self._performance_metrics
|
||||
tokens = response.get("tokens", {})
|
||||
|
||||
metrics["total_tokens"] += tokens.get("total", 0)
|
||||
metrics["prompt_tokens"] += tokens.get("prompt", 0)
|
||||
metrics["completion_tokens"] += tokens.get("completion", 0)
|
||||
metrics["successful_requests"] += 1
|
||||
|
||||
metrics["average_latency"] = (
|
||||
(metrics["average_latency"] * (metrics["successful_requests"] - 1) + latency)
|
||||
/ metrics["successful_requests"]
|
||||
)
|
||||
metrics["max_latency"] = max(metrics["max_latency"], latency)
|
||||
metrics["min_latency"] = min(metrics["min_latency"], latency)
|
||||
|
||||
def _update_history(self, question: str, response: dict) -> None:
|
||||
"""Update conversation history."""
|
||||
self._conversation_history.append({
|
||||
"timestamp": dt_util.utcnow().isoformat(),
|
||||
"question": question,
|
||||
"response": response["content"]
|
||||
})
|
||||
|
||||
while len(self._conversation_history) > self.max_history_size:
|
||||
self._conversation_history.pop(0)
|
||||
|
||||
def _handle_error(self, error: Exception) -> None:
|
||||
"""Handle error and update metrics."""
|
||||
self._performance_metrics["total_errors"] += 1
|
||||
self._performance_metrics["failed_requests"] += 1
|
||||
|
||||
self.last_response = {
|
||||
"timestamp": dt_util.utcnow().isoformat(),
|
||||
"question": "",
|
||||
"response": "",
|
||||
"model": self.model,
|
||||
"instance": self.instance_name,
|
||||
"error": str(error)
|
||||
}
|
||||
|
||||
async def async_clear_history(self) -> None:
|
||||
"""Clear conversation history."""
|
||||
self._conversation_history = []
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_get_history(self) -> List[Dict[str, str]]:
|
||||
"""Get conversation history."""
|
||||
return self._conversation_history
|
||||
|
||||
async def async_set_system_prompt(self, prompt: str) -> None:
|
||||
"""Set system prompt."""
|
||||
self._system_prompt = prompt
|
||||
await self.async_update_ha_state()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 678 KiB |
@@ -1,14 +1,28 @@
|
||||
{
|
||||
"domain": "ha_text_ai",
|
||||
"name": "HA Text AI",
|
||||
"after_dependencies": ["http"],
|
||||
"bluetooth": [],
|
||||
"codeowners": ["@smkrv"],
|
||||
"config_flow": true,
|
||||
"dependencies": [],
|
||||
"documentation": "https://github.com/smkrv/ha-text-ai/wiki",
|
||||
"documentation": "https://github.com/smkrv/ha-text-ai",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
|
||||
"requirements": ["openai>=1.0.0"],
|
||||
"loggers": ["custom_components.ha_text_ai"],
|
||||
"mqtt": [],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"openai>=1.12.0",
|
||||
"anthropic>=0.8.0",
|
||||
"aiohttp>=3.8.0",
|
||||
"async-timeout>=4.0.0",
|
||||
"certifi>=2024.2.2"
|
||||
],
|
||||
"single_config_entry": false,
|
||||
"ssdp": [],
|
||||
"version": "1.0.3",
|
||||
"usb": [],
|
||||
"version": "2.0.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
|
||||
@@ -1,30 +1,65 @@
|
||||
"""Sensor platform for HA text AI."""
|
||||
from datetime import datetime
|
||||
"""Sensor platform for HA Text AI."""
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
import math
|
||||
from typing import Any, Dict
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
SensorDeviceClass,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
ATTR_QUESTION,
|
||||
ATTR_RESPONSE,
|
||||
ATTR_LAST_UPDATED,
|
||||
ATTR_MODEL,
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_MAX_TOKENS,
|
||||
CONF_MODEL,
|
||||
CONF_API_PROVIDER,
|
||||
ATTR_TOTAL_RESPONSES,
|
||||
ATTR_TOTAL_ERRORS,
|
||||
ATTR_AVG_RESPONSE_TIME,
|
||||
ATTR_LAST_REQUEST_TIME,
|
||||
ATTR_LAST_ERROR,
|
||||
ATTR_IS_PROCESSING,
|
||||
ATTR_IS_RATE_LIMITED,
|
||||
ATTR_IS_MAINTENANCE,
|
||||
ATTR_API_VERSION,
|
||||
ATTR_ENDPOINT_STATUS,
|
||||
ATTR_PERFORMANCE_METRICS,
|
||||
ATTR_HISTORY_SIZE,
|
||||
ATTR_UPTIME,
|
||||
ATTR_API_PROVIDER,
|
||||
ATTR_MODEL,
|
||||
ATTR_SYSTEM_PROMPT,
|
||||
ATTR_API_STATUS,
|
||||
ATTR_RESPONSE,
|
||||
ATTR_QUESTION,
|
||||
ATTR_CONVERSATION_HISTORY,
|
||||
METRIC_TOTAL_TOKENS,
|
||||
METRIC_PROMPT_TOKENS,
|
||||
METRIC_COMPLETION_TOKENS,
|
||||
METRIC_SUCCESSFUL_REQUESTS,
|
||||
METRIC_FAILED_REQUESTS,
|
||||
METRIC_AVERAGE_LATENCY,
|
||||
METRIC_MAX_LATENCY,
|
||||
METRIC_MIN_LATENCY,
|
||||
STATE_READY,
|
||||
STATE_PROCESSING,
|
||||
STATE_ERROR,
|
||||
STATE_INITIALIZING,
|
||||
STATE_MAINTENANCE,
|
||||
STATE_RATE_LIMITED,
|
||||
STATE_DISCONNECTED,
|
||||
ENTITY_ICON,
|
||||
ENTITY_ICON_ERROR,
|
||||
ENTITY_ICON_PROCESSING,
|
||||
)
|
||||
|
||||
from .coordinator import HATextAICoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -34,17 +69,19 @@ async def async_setup_entry(
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the HA text AI sensor."""
|
||||
"""Set up the HA Text AI sensor."""
|
||||
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):
|
||||
"""HA text AI Sensor."""
|
||||
"""HA Text AI Sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_device_class = SensorDeviceClass.TIMESTAMP
|
||||
_attr_icon = "mdi:robot"
|
||||
coordinator: HATextAICoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -53,86 +90,205 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
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_name = "Last Response"
|
||||
self._attr_suggested_display_precision = 0
|
||||
|
||||
@property
|
||||
def state(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
if not self.coordinator.data or not self.coordinator.last_update_success_time:
|
||||
return None
|
||||
self.entity_description = SensorEntityDescription(
|
||||
key=f"ha_text_ai_{self._instance_name}",
|
||||
entity_registry_enabled_default=True,
|
||||
)
|
||||
|
||||
# Convert to local time
|
||||
if isinstance(self.coordinator.last_update_success_time, datetime):
|
||||
return dt_util.as_local(self.coordinator.last_update_success_time)
|
||||
return self.coordinator.last_update_success_time
|
||||
self._current_state = STATE_INITIALIZING
|
||||
self._error_count = 0
|
||||
self._last_error = None
|
||||
self._last_update = None
|
||||
self._is_processing = False
|
||||
self._last_response = {}
|
||||
self._metrics = {}
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Optional[Dict[str, Any]]:
|
||||
"""Return entity specific state attributes."""
|
||||
if not self.coordinator.data:
|
||||
return {
|
||||
ATTR_TOTAL_RESPONSES: 0,
|
||||
ATTR_MODEL: self.coordinator.model,
|
||||
ATTR_TEMPERATURE: self.coordinator.temperature,
|
||||
ATTR_MAX_TOKENS: self.coordinator.max_tokens,
|
||||
}
|
||||
model = config_entry.data.get(CONF_MODEL, "Unknown")
|
||||
api_provider = config_entry.data.get(CONF_API_PROVIDER, "Unknown")
|
||||
|
||||
try:
|
||||
history = list(self.coordinator.data.items())
|
||||
if not history:
|
||||
return {
|
||||
ATTR_TOTAL_RESPONSES: 0,
|
||||
ATTR_MODEL: self.coordinator.model,
|
||||
ATTR_TEMPERATURE: self.coordinator.temperature,
|
||||
ATTR_MAX_TOKENS: self.coordinator.max_tokens,
|
||||
}
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
name=self._attr_name, # Используем имя сенсора
|
||||
manufacturer="Community",
|
||||
model=f"{model} ({api_provider} provider)",
|
||||
sw_version="1.0.0",
|
||||
)
|
||||
|
||||
last_question, last_data = history[-1]
|
||||
|
||||
# Handle different response formats
|
||||
if isinstance(last_data, dict):
|
||||
last_response = last_data.get("response", "")
|
||||
last_updated = last_data.get("timestamp", self.coordinator.last_update_success_time)
|
||||
else:
|
||||
last_response = str(last_data)
|
||||
last_updated = self.coordinator.last_update_success_time
|
||||
|
||||
# Convert timestamp to local time if needed
|
||||
if isinstance(last_updated, datetime):
|
||||
last_updated = dt_util.as_local(last_updated)
|
||||
|
||||
return {
|
||||
ATTR_QUESTION: last_question,
|
||||
ATTR_RESPONSE: last_response,
|
||||
ATTR_LAST_UPDATED: last_updated,
|
||||
ATTR_TOTAL_RESPONSES: len(history),
|
||||
ATTR_MODEL: self.coordinator.model,
|
||||
ATTR_TEMPERATURE: self.coordinator.temperature,
|
||||
ATTR_MAX_TOKENS: self.coordinator.max_tokens,
|
||||
}
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error getting attributes: %s", err, exc_info=True)
|
||||
return {
|
||||
ATTR_TOTAL_RESPONSES: 0,
|
||||
ATTR_MODEL: self.coordinator.model,
|
||||
ATTR_TEMPERATURE: self.coordinator.temperature,
|
||||
ATTR_MAX_TOKENS: self.coordinator.max_tokens,
|
||||
}
|
||||
_LOGGER.debug(f"Initialized sensor: {self.entity_id} for instance: {self._instance_name}")
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""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 should_poll(self) -> bool:
|
||||
"""No need to poll. Coordinator notifies entity of updates."""
|
||||
return False
|
||||
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:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self._handle_coordinator_update()
|
||||
_LOGGER.debug(f"Entity {self.entity_id} added to Home Assistant")
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
try:
|
||||
data = self.coordinator.data
|
||||
if not self.coordinator.last_update_success or not data:
|
||||
self._current_state = STATE_DISCONNECTED
|
||||
_LOGGER.warning(f"No data available for {self.entity_id}")
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._is_processing = data.get("is_processing", False)
|
||||
|
||||
# Update conversation history and system prompt
|
||||
self._conversation_history = data.get("conversation_history", [])
|
||||
self._system_prompt = data.get("system_prompt")
|
||||
|
||||
# Update state based on conditions
|
||||
if self._is_processing:
|
||||
self._current_state = STATE_PROCESSING
|
||||
elif data.get("is_rate_limited"):
|
||||
self._current_state = STATE_RATE_LIMITED
|
||||
elif data.get("is_maintenance"):
|
||||
self._current_state = STATE_MAINTENANCE
|
||||
elif data.get("error"):
|
||||
self._current_state = STATE_ERROR
|
||||
self._last_error = data["error"]
|
||||
self._error_count += 1
|
||||
else:
|
||||
self._current_state = data.get("state", STATE_READY)
|
||||
|
||||
# Update last update timestamp
|
||||
self._last_update = dt_util.utcnow()
|
||||
|
||||
_LOGGER.debug(
|
||||
f"Updated {self.entity_id} state to: {self._current_state} "
|
||||
f"(available: {self.available})"
|
||||
)
|
||||
|
||||
except Exception as err:
|
||||
self._current_state = STATE_ERROR
|
||||
self._last_error = str(err)
|
||||
self._error_count += 1
|
||||
_LOGGER.error(
|
||||
"Error handling update for %s: %s",
|
||||
self.entity_id,
|
||||
err,
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -4,46 +4,55 @@ ask_question:
|
||||
Send a question to the AI model and receive a detailed response.
|
||||
The response will be stored in the conversation history and can be retrieved later.
|
||||
fields:
|
||||
instance:
|
||||
name: Instance
|
||||
description: Name of the HA Text AI instance to use
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
integration: ha_text_ai
|
||||
domain: sensor
|
||||
|
||||
question:
|
||||
name: Question
|
||||
description: >-
|
||||
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.
|
||||
description: Your question or prompt for the AI assistant
|
||||
required: true
|
||||
example: |
|
||||
What automations would you recommend for a smart kitchen?
|
||||
Consider energy efficiency and convenience.
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
type: text
|
||||
|
||||
system_prompt:
|
||||
name: System Prompt
|
||||
description: Optional system prompt to set context for this specific question
|
||||
required: false
|
||||
selector:
|
||||
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:
|
||||
name: Model
|
||||
description: >-
|
||||
Select an AI model to use (optional, overrides default setting).
|
||||
Different models have different capabilities and token limits.
|
||||
description: "Select AI model to use (optional, overrides default setting)"
|
||||
required: false
|
||||
example: "gpt-3.5-turbo"
|
||||
default: "gpt-3.5-turbo"
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: "GPT-3.5 Turbo (Fast & Efficient)"
|
||||
value: "gpt-3.5-turbo"
|
||||
- label: "GPT-4 (Most Capable)"
|
||||
value: "gpt-4"
|
||||
- label: "GPT-4 32K (Extended Context)"
|
||||
value: "gpt-4-32k"
|
||||
mode: dropdown
|
||||
selector:
|
||||
text:
|
||||
multiline: false
|
||||
|
||||
temperature:
|
||||
name: Temperature
|
||||
description: >-
|
||||
Controls response creativity (0-2):
|
||||
0.0-0.3: Focused, consistent responses
|
||||
0.4-0.7: Balanced responses
|
||||
0.8-2.0: More creative, varied responses
|
||||
description: Controls response creativity (0.0-2.0)
|
||||
required: false
|
||||
default: 0.7
|
||||
selector:
|
||||
@@ -52,16 +61,10 @@ ask_question:
|
||||
max: 2.0
|
||||
step: 0.1
|
||||
mode: slider
|
||||
unit_of_measurement: ""
|
||||
|
||||
max_tokens:
|
||||
name: Max Tokens
|
||||
description: >-
|
||||
Maximum length of the response. Higher values allow longer responses but use more API tokens.
|
||||
Recommended ranges:
|
||||
- Short responses: 256-512
|
||||
- Medium responses: 512-1024
|
||||
- Long responses: 1024-4096
|
||||
description: Maximum length of the response (1-4096 tokens)
|
||||
required: false
|
||||
default: 1000
|
||||
selector:
|
||||
@@ -70,54 +73,3 @@ ask_question:
|
||||
max: 4096
|
||||
step: 1
|
||||
mode: box
|
||||
|
||||
clear_history:
|
||||
name: Clear History
|
||||
description: >-
|
||||
Delete all stored questions and responses from the conversation history.
|
||||
This action cannot be undone.
|
||||
fields: {}
|
||||
|
||||
get_history:
|
||||
name: Get History
|
||||
description: >-
|
||||
Retrieve recent conversation history, including questions, responses, and timestamps.
|
||||
Results are ordered from newest to oldest.
|
||||
fields:
|
||||
limit:
|
||||
name: Limit
|
||||
description: >-
|
||||
Number of most recent conversations to return.
|
||||
Higher values return more history but may take longer to process.
|
||||
required: false
|
||||
default: 10
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 100
|
||||
step: 1
|
||||
mode: box
|
||||
|
||||
set_system_prompt:
|
||||
name: Set System Prompt
|
||||
description: >-
|
||||
Configure the AI's behavior by setting a system prompt.
|
||||
This affects how the AI interprets and responds to all future questions.
|
||||
fields:
|
||||
prompt:
|
||||
name: System Prompt
|
||||
description: >-
|
||||
Instructions that define how the AI should behave and respond.
|
||||
Be specific about the desired expertise, tone, and format of responses.
|
||||
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.
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
type: text
|
||||
|
||||
@@ -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,142 +1,260 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up HA text AI",
|
||||
"description": "Configure your OpenAI integration for smart home interactions. You'll need an OpenAI API key to proceed.",
|
||||
"data": {
|
||||
"api_key": {
|
||||
"name": "OpenAI API Key",
|
||||
"description": "Your OpenAI API key from platform.openai.com"
|
||||
},
|
||||
"model": {
|
||||
"name": "AI Model",
|
||||
"description": "Select the AI model to use. GPT-3.5-Turbo is recommended for most uses."
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature",
|
||||
"description": "Controls response creativity (0-2). Lower values for focused responses, higher for creative ones."
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Max Tokens",
|
||||
"description": "Maximum length of responses. Higher values allow longer responses but use more API tokens."
|
||||
},
|
||||
"api_endpoint": {
|
||||
"name": "API Endpoint",
|
||||
"description": "OpenAI API endpoint URL. Leave default unless using a custom endpoint."
|
||||
},
|
||||
"request_interval": {
|
||||
"name": "Request Interval",
|
||||
"description": "Minimum time between API requests in seconds. Increase if hitting rate limits."
|
||||
}
|
||||
"config": {
|
||||
"step": {
|
||||
"provider": {
|
||||
"title": "Select AI Provider",
|
||||
"description": "Choose which AI service provider to use for this instance",
|
||||
"data": {
|
||||
"api_provider": "API Provider",
|
||||
"context_messages": "Number of context messages to retain (1-20)"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"title": "Configure HA Text AI Instance",
|
||||
"description": "Set up a new AI assistant instance with your selected provider",
|
||||
"data": {
|
||||
"name": "Instance name (e.g., 'GPT Assistant', 'Claude Helper')",
|
||||
"api_key": "API key for authentication",
|
||||
"model": "AI model to use",
|
||||
"temperature": "Response creativity (0-2, lower = more focused)",
|
||||
"max_tokens": "Maximum response length (1-4096 tokens)",
|
||||
"api_endpoint": "Custom API endpoint URL (optional)",
|
||||
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
||||
"context_messages": "Number of context messages to retain (1-20)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "An instance with this name already exists",
|
||||
"invalid_name": "Invalid instance name",
|
||||
"invalid_auth": "Authentication failed - check your API key",
|
||||
"invalid_api_key": "Invalid API key - please verify your credentials",
|
||||
"cannot_connect": "Failed to connect to API service",
|
||||
"invalid_model": "Selected model is not available",
|
||||
"rate_limit": "Rate limit exceeded",
|
||||
"context_length": "Context length exceeded",
|
||||
"rate_limit_exceeded": "API rate limit exceeded",
|
||||
"maintenance": "Service is under maintenance",
|
||||
"invalid_response": "Invalid API response received",
|
||||
"api_error": "API service error occurred",
|
||||
"timeout": "Request timed out",
|
||||
"invalid_instance": "Invalid instance specified",
|
||||
"unknown": "Unexpected error occurred"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Invalid API key. Please check your OpenAI API key and try again.",
|
||||
"cannot_connect": "Failed to connect to API. Please check your internet connection and API endpoint.",
|
||||
"unknown": "Unexpected error occurred. Please check the logs for more details.",
|
||||
"already_exists": "This API key is already configured in another integration.",
|
||||
"invalid_model": "Selected model is not available. Please choose a different model.",
|
||||
"rate_limit": "API rate limit exceeded. Please try again later or increase the request interval.",
|
||||
"context_length": "Input too long for selected model. Try reducing max tokens or using a model with larger context.",
|
||||
"api_error": "OpenAI API error. Please check the logs for details."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This OpenAI integration is already configured",
|
||||
"auth_failed": "Authentication failed. Please verify your API key.",
|
||||
"invalid_endpoint": "Invalid API endpoint URL provided"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "HA text AI Options",
|
||||
"description": "Adjust your OpenAI integration settings. Changes will apply to future requests.",
|
||||
"data": {
|
||||
"temperature": {
|
||||
"name": "Temperature",
|
||||
"description": "Controls response creativity (0-2). Lower values for focused responses, higher for creative ones."
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Max Tokens",
|
||||
"description": "Maximum length of responses. Higher values allow longer responses but use more API tokens."
|
||||
},
|
||||
"request_interval": {
|
||||
"name": "Request Interval",
|
||||
"description": "Minimum time between API requests in seconds. Increase if hitting rate limits."
|
||||
}
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Update Instance Settings",
|
||||
"description": "Modify settings for this AI assistant instance",
|
||||
"data": {
|
||||
"model": "AI model",
|
||||
"temperature": "Response creativity (0-2)",
|
||||
"max_tokens": "Maximum response length (1-4096)",
|
||||
"request_interval": "Minimum request interval (0.1-60 seconds)",
|
||||
"context_messages": "Number of previous messages to include in context (1-20)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"last_response": {
|
||||
"name": "Last Response",
|
||||
"state_attributes": {
|
||||
"last_updated": {
|
||||
"name": "Last Updated",
|
||||
"description": "Time of the last AI response"
|
||||
},
|
||||
"question": {
|
||||
"name": "Last Question",
|
||||
"description": "Most recent question asked"
|
||||
},
|
||||
"response": {
|
||||
"name": "AI Response",
|
||||
"description": "Latest response from the AI"
|
||||
},
|
||||
"model": {
|
||||
"name": "Current Model",
|
||||
"description": "AI model currently in use"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature Setting",
|
||||
"description": "Current temperature parameter"
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Max Tokens Setting",
|
||||
"description": "Current maximum tokens limit"
|
||||
},
|
||||
"total_responses": {
|
||||
"name": "Total Responses",
|
||||
"description": "Number of responses since last reset"
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "System Prompt",
|
||||
"description": "Current system instructions for the AI"
|
||||
},
|
||||
"response_time": {
|
||||
"name": "Response Time",
|
||||
"description": "Time taken to generate last response"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"name": "Ask Question (HA Text AI)",
|
||||
"description": "Send a question to the AI model and receive a detailed response. The response will be stored in the conversation history and can be retrieved later.",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instance",
|
||||
"description": "Name of the HA Text AI instance to use"
|
||||
},
|
||||
"question": {
|
||||
"name": "Question",
|
||||
"description": "Your question or prompt for the AI assistant"
|
||||
},
|
||||
"context_messages": {
|
||||
"name": "Context Messages",
|
||||
"description": "Number of previous messages to include in context (1-20)"
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "System Prompt",
|
||||
"description": "Optional system prompt to set context for this specific question"
|
||||
},
|
||||
"model": {
|
||||
"name": "Model",
|
||||
"description": "Select AI model to use (optional, overrides default setting)"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature",
|
||||
"description": "Controls response creativity (0.0-2.0)"
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Max Tokens",
|
||||
"description": "Maximum length of the response (1-4096 tokens)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"clear_history": {
|
||||
"name": "Clear History",
|
||||
"description": "Delete all stored questions and responses from the conversation history",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instance",
|
||||
"description": "Name of the HA Text AI instance to clear history for"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_history": {
|
||||
"name": "Get History",
|
||||
"description": "Retrieve conversation history with optional filtering and sorting",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instance",
|
||||
"description": "Name of the HA Text AI instance to get history from"
|
||||
},
|
||||
"limit": {
|
||||
"name": "Limit",
|
||||
"description": "Number of conversations to return (1-100)"
|
||||
},
|
||||
"filter_model": {
|
||||
"name": "Filter Model",
|
||||
"description": "Filter conversations by specific AI model"
|
||||
},
|
||||
"start_date": {
|
||||
"name": "Start Date",
|
||||
"description": "Filter conversations starting from this date/time"
|
||||
},
|
||||
"include_metadata": {
|
||||
"name": "Include Metadata",
|
||||
"description": "Include additional information like tokens used, response time, etc."
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "Sort Order",
|
||||
"description": "Sort order for results (newest or oldest first)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_system_prompt": {
|
||||
"name": "Set System Prompt",
|
||||
"description": "Set default system behavior instructions for all future conversations",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Instance",
|
||||
"description": "Name of the HA Text AI instance to set system prompt for"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "System Prompt",
|
||||
"description": "Instructions that define how the AI should behave and respond"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"name": "Ask Question",
|
||||
"description": "Send a question to the AI model",
|
||||
"fields": {
|
||||
"question": {
|
||||
"name": "Question",
|
||||
"description": "Your question for the AI"
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"ha_text_ai": {
|
||||
"name": "{name}",
|
||||
"state": {
|
||||
"ready": "Ready",
|
||||
"processing": "Processing",
|
||||
"error": "Error",
|
||||
"disconnected": "Disconnected",
|
||||
"rate_limited": "Rate Limited",
|
||||
"maintenance": "Maintenance",
|
||||
"initializing": "Initializing",
|
||||
"retrying": "Retrying",
|
||||
"queued": "Queued"
|
||||
},
|
||||
"state_attributes": {
|
||||
"question": {
|
||||
"name": "Last Question"
|
||||
},
|
||||
"response": {
|
||||
"name": "Last Response"
|
||||
},
|
||||
"model": {
|
||||
"name": "Current Model"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature"
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Max Tokens"
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "System Prompt"
|
||||
},
|
||||
"response_time": {
|
||||
"name": "Last Response Time"
|
||||
},
|
||||
"total_responses": {
|
||||
"name": "Total Responses"
|
||||
},
|
||||
"error_count": {
|
||||
"name": "Error Count"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "Last Error"
|
||||
},
|
||||
"api_status": {
|
||||
"name": "API Status"
|
||||
},
|
||||
"tokens_used": {
|
||||
"name": "Total Tokens Used"
|
||||
},
|
||||
"average_response_time": {
|
||||
"name": "Average Response Time"
|
||||
},
|
||||
"last_request_time": {
|
||||
"name": "Last Request Time"
|
||||
},
|
||||
"is_processing": {
|
||||
"name": "Processing Status"
|
||||
},
|
||||
"is_rate_limited": {
|
||||
"name": "Rate Limited Status"
|
||||
},
|
||||
"is_maintenance": {
|
||||
"name": "Maintenance Status"
|
||||
},
|
||||
"api_version": {
|
||||
"name": "API Version"
|
||||
},
|
||||
"endpoint_status": {
|
||||
"name": "Endpoint Status"
|
||||
},
|
||||
"performance_metrics": {
|
||||
"name": "Performance Metrics"
|
||||
},
|
||||
"history_size": {
|
||||
"name": "History Size"
|
||||
},
|
||||
"uptime": {
|
||||
"name": "Uptime"
|
||||
},
|
||||
"total_tokens": {
|
||||
"name": "Total Tokens"
|
||||
},
|
||||
"prompt_tokens": {
|
||||
"name": "Prompt Tokens"
|
||||
},
|
||||
"completion_tokens": {
|
||||
"name": "Completion Tokens"
|
||||
},
|
||||
"successful_requests": {
|
||||
"name": "Successful Requests"
|
||||
},
|
||||
"failed_requests": {
|
||||
"name": "Failed Requests"
|
||||
},
|
||||
"average_latency": {
|
||||
"name": "Average Latency"
|
||||
},
|
||||
"max_latency": {
|
||||
"name": "Maximum Latency"
|
||||
},
|
||||
"min_latency": {
|
||||
"name": "Minimum Latency"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"clear_history": {
|
||||
"name": "Clear History",
|
||||
"description": "Clear conversation history"
|
||||
},
|
||||
"get_history": {
|
||||
"name": "Get History",
|
||||
"description": "Retrieve conversation history"
|
||||
},
|
||||
"set_system_prompt": {
|
||||
"name": "Set System Prompt",
|
||||
"description": "Set AI behavior instructions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Минимальная задержка"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "HA text AI",
|
||||
"render_readme": true,
|
||||
"icon": "mdi:robot",
|
||||
"domains": ["sensor"],
|
||||
"homeassistant": "2024.11.0",
|
||||
"icon": "mdi:brain",
|
||||
"version": "1.0.3",
|
||||
"version": "2.0.0",
|
||||
"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
|
||||
@@ -0,0 +1,19 @@
|
||||
```
|
||||
ha-text-ai/
|
||||
│
|
||||
├── custom_components/
|
||||
│ └── ha_text_ai/
|
||||
│ ├── __init__.py
|
||||
│ ├── config_flow.py
|
||||
│ ├── coordinator.py
|
||||
│ ├── manifest.json
|
||||
│ ├── sensor.py
|
||||
│ ├── services.yaml
|
||||
│ ├── const.py
|
||||
│ └── api_client.py
|
||||
│
|
||||
└── strings/
|
||||
├── en.json
|
||||
├── de.json
|
||||
└── ru.json
|
||||
```
|
||||
Reference in New Issue
Block a user