Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e91c3701c5 | ||
|
|
7f62101b3e | ||
|
|
3729c3736f | ||
|
|
f5ce5e459a | ||
|
|
4064486b1e | ||
|
|
185778dbd0 | ||
|
|
83a255dee0 | ||
|
|
6b66dd6a4d | ||
|
|
bd82f23120 | ||
|
|
eee9754033 | ||
|
|
517b1f11ae | ||
|
|
ed8f19bfa9 | ||
|
|
7e3daf611b | ||
|
|
37919be70f | ||
|
|
e427254584 | ||
|
|
76c5629fa0 | ||
|
|
7958bd010b | ||
|
|
8cd876195a | ||
|
|
376753e001 | ||
|
|
b6e73e847d | ||
|
|
440c734214 | ||
|
|
73788373cd | ||
|
|
4bfc96019b | ||
|
|
2138fc7654 | ||
|
|
95bd2ebb41 | ||
|
|
cad0fd7031 | ||
|
|
c003b258f6 | ||
|
|
65a10c77f4 | ||
|
|
e1463828c9 | ||
|
|
5ebb9c9c66 | ||
|
|
f17c631a79 | ||
|
|
0e06794384 | ||
|
|
d8a924909b | ||
|
|
29f1659a02 | ||
|
|
5b7905de80 | ||
|
|
cf9ac6dcea | ||
|
|
568eb3e16c | ||
|
|
53fb150389 | ||
|
|
acbb53d2af | ||
|
|
e19db29441 | ||
|
|
bfd64d1122 | ||
|
|
82e1f0c4f9 | ||
|
|
5c16eee6e4 | ||
|
|
e988d445a4 | ||
|
|
f9bfb9ab7f | ||
|
|
92dd1bc110 | ||
|
|
17d547325a | ||
|
|
b8cb70217c | ||
|
|
530d04f25d | ||
|
|
b71083b9bf | ||
|
|
f9f7d10f7f | ||
|
|
9f7cb20621 | ||
|
|
6fc3b23365 | ||
|
|
15c717fcb0 | ||
|
|
be06fddce1 | ||
|
|
5f0bd861a7 | ||
|
|
428aee46c8 | ||
|
|
561bcf0b1d | ||
|
|
2b1e42c665 | ||
|
|
2d68f29ab5 | ||
|
|
19d4a93bca | ||
|
|
fb75c5f44e | ||
|
|
52af987985 | ||
|
|
162a30acdd | ||
|
|
898a6fc638 | ||
|
|
38e2362be4 | ||
|
|
cea912d0b5 | ||
|
|
5ec00040c2 | ||
|
|
8d28fc4a0d | ||
|
|
7cc6587724 | ||
|
|
bacf76e0a9 | ||
|
|
f86c7bfd57 | ||
|
|
2aaf340575 | ||
|
|
bae11ba85c | ||
|
|
9eb7d8912c | ||
|
|
d29535245f | ||
|
|
ca3ae982b0 | ||
|
|
0c4399b46c | ||
|
|
c13ef1921d | ||
|
|
0fb9acfa8f | ||
|
|
2ac4389e1a | ||
|
|
de194e425c | ||
|
|
95f3d6506e | ||
|
|
3e877243b9 | ||
|
|
fd795c9e90 | ||
|
|
5f5dc041b9 | ||
|
|
3d3885d43d | ||
|
|
b059744716 | ||
|
|
f2a41aaa2c | ||
|
|
1fcf751d0d | ||
|
|
a45c9407fd | ||
|
|
21fdf108cf | ||
|
|
c7ec2b3ea4 | ||
|
|
6c4da6cea5 | ||
|
|
3029c5dd26 | ||
|
|
3e97028094 | ||
|
|
1194c87134 | ||
|
|
e03b5315c2 | ||
|
|
fad887492f | ||
|
|
f26df29937 | ||
|
|
69b54a08ba | ||
|
|
7ce77eb18e | ||
|
|
1480669dc8 |
@@ -1,5 +1,6 @@
|
|||||||
name: Validate with hassfest
|
name: Validate with hassfest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
name: Validate
|
name: Validate
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
# 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
|
|
||||||
Thumbs.db
|
|
||||||
*.psd
|
|
||||||
*.zip
|
|
||||||
*.txt
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
issue tracker.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
@@ -2,99 +2,156 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
  [](https://creativecommons.org/licenses/by-nc-sa/4.0/) [](https://github.com/hacs/integration)   
|
  [](https://creativecommons.org/licenses/by-nc-sa/4.0/) [](https://github.com/hacs/integration)
|
||||||
|
       
|
||||||
|
|
||||||
<img src="https://github.com/smkrv/ha-text-ai/blob/524849f6a945ec62c2cf6a6b7ecd9a28b37bf0fa/misc/icons/logo.jpg" alt="HA Text AI" height="160"/>
|
|
||||||
|
<img src="https://github.com/smkrv/ha-text-ai/blob/main/custom_components/ha_text_ai/icons/logo%402x.png" alt="HA Text AI" style="width: 50%; max-width: 256px; max-height: 128px; aspect-ratio: 2/1; object-fit: contain;"/>
|
||||||
|
|
||||||
### Advanced AI Integration for [Home Assistant](https://www.home-assistant.io/) with LLM multi-provider support
|
### Advanced AI Integration for [Home Assistant](https://www.home-assistant.io/) with LLM multi-provider support
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Transform your smart home experience with powerful AI assistance powered by multiple AI providers including OpenAI GPT and Anthropic Claude models. Get intelligent responses, automate complex scenarios, and enhance your home automation with advanced natural language processing.
|
Transform your smart home experience with powerful AI assistance powered by multiple AI providers including OpenAI GPT, DeepSeek and Anthropic Claude models. Get intelligent responses, automate complex scenarios, and enhance your home automation with advanced natural language processing.
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> 🚧 BETA VERSION
|
> 🤝 Community Driven: for more details on the integration,
|
||||||
> Expect: potential bugs, frequent changes, incomplete features.
|
> check out the discussion on the **[Home Assistant Community forum](https://community.home-assistant.io/t/ha-text-ai-transforming-home-automation-through-multi-llm-integration/799741)**
|
||||||
> 🤝 Community Driven
|
|
||||||
>
|
>
|
||||||
> <a href="https://community.home-assistant.io/t/ha-text-ai-transforming-home-automation-with-multi-provider-language-models/799741"><img src="https://img.shields.io/badge/Community-blue?style=for-the-badge&logo=homeassistant&logoColor=white&color=03a9f4"/></a>
|
> <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="210" height="auto"></a>
|
||||||
>
|
>
|
||||||
> [Screenshots](misc/screenshots/screenshot.jpg)
|
> [Screenshots](assets/images/screenshots/screenshot.jpg)
|
||||||
|
|
||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
- 🧠 **Multi-Provider AI Integration**:
|
- 🧠 **Multi-Provider AI Integration**: Support for OpenAI GPT, DeepSeek and Anthropic Claude models
|
||||||
- Support for OpenAI GPT models
|
- 💬 **Advanced Language Processing**: Context-aware, multi-turn conversations
|
||||||
- Anthropic Claude integration
|
- 📝 **Enhanced Memory Management**: Secure file-based history storage
|
||||||
- Custom API endpoints
|
- ⚡ **Performance Optimization**: Efficient token usage and smart rate limiting
|
||||||
- Flexible model selection
|
- 🎯 **Advanced Customization**: Per-request model and parameter selection
|
||||||
|
- 🔒 **Enhanced Security**: Secure API key management and usage monitoring
|
||||||
|
- 🎨 **Improved User Experience**: Intuitive configuration and rich interfaces
|
||||||
|
- 🔄 **Automation Integration**: Event-driven responses and template compatibility
|
||||||
|
|
||||||
- 💬 **Advanced Language Processing**:
|
<details>
|
||||||
- Context-aware responses
|
<summary>📦 Detailed Feature Breakdown</summary>
|
||||||
- Multi-turn conversations
|
|
||||||
- Custom system instructions
|
|
||||||
- Natural conversation flow
|
|
||||||
|
|
||||||
- 📝 **Enhanced Memory Management**:
|
### 🧠 **Multi-Provider AI Integration**
|
||||||
- File-based conversation history storage
|
- Support for OpenAI GPT models
|
||||||
- Automatic history rotation
|
- Anthropic Claude integration
|
||||||
- Configurable history size limits
|
- DeepSeek integration
|
||||||
- Secure storage in Home Assistant
|
- Custom API endpoints
|
||||||
|
- Flexible model selection
|
||||||
|
|
||||||
- ⚡ **Performance Optimization**:
|
### 💬 **Advanced Language Processing**
|
||||||
- Efficient token usage
|
- Context-aware responses
|
||||||
- Smart rate limiting
|
- Multi-turn conversations
|
||||||
- Response caching
|
- Custom system instructions
|
||||||
- Request interval control
|
- Natural conversation flow
|
||||||
|
|
||||||
- 🎯 **Advanced Customization**:
|
### 📝 **Enhanced Memory Management**
|
||||||
- Per-request model selection
|
- File-based conversation history storage
|
||||||
- Adjustable parameters
|
- Automatic history rotation
|
||||||
- Custom system prompts
|
- Configurable history size limits
|
||||||
- Temperature control
|
- Secure storage in Home Assistant
|
||||||
|
|
||||||
- 🔒 **Enhanced Security**:
|
### ⚡ **Performance Optimization**
|
||||||
- Secure API key storage
|
- Efficient token usage
|
||||||
- Rate limiting protection
|
- Smart rate limiting
|
||||||
- Error handling
|
- Response caching
|
||||||
- Usage monitoring
|
- Request interval control
|
||||||
|
|
||||||
- 🎨 **Improved User Experience**:
|
### 🎯 **Advanced Customization**
|
||||||
- Intuitive configuration UI
|
- Per-request model selection
|
||||||
- Detailed sensor attributes
|
- Adjustable parameters
|
||||||
- Rich service interface
|
- Custom system prompts
|
||||||
- Model selection UI
|
- Temperature control
|
||||||
|
|
||||||
- 🔄 **Automation Integration**:
|
### 🔒 **Enhanced Security**
|
||||||
- Event-driven responses
|
- Secure API key storage
|
||||||
- Conditional logic support
|
- Rate limiting protection
|
||||||
- Template compatibility
|
- Error handling
|
||||||
- Model-specific automation
|
- 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
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
#### 🌐 Translations
|
||||||
|
|
||||||
|
| Code | Language | Status |
|
||||||
|
|------|----------|--------|
|
||||||
|
| 🇩🇪 de | Deutsch | Full |
|
||||||
|
| 🇬🇧 en | English | Primary |
|
||||||
|
| 🇪🇸 es | Español | Full |
|
||||||
|
| 🇮🇳 hi | हिन्दी | Full |
|
||||||
|
| 🇮🇹 it | Italiano | Full |
|
||||||
|
| 🇷🇺 ru | Русский | Full |
|
||||||
|
| 🇷🇸 sr | Српски | Full |
|
||||||
|
| 🇨🇳 zh | 中文 | Full |
|
||||||
|
|
||||||
## 📋 Prerequisites
|
## 📋 Prerequisites
|
||||||
|
|
||||||
- Home Assistant 2024.11 or later
|
- Home Assistant 2024.12.0 or later (recommended for best compatibility)
|
||||||
- Active API key from:
|
- Active API key from:
|
||||||
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
|
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
|
||||||
- Anthropic ([Get key](https://console.anthropic.com/))
|
- Anthropic ([Get key](https://console.anthropic.com/))
|
||||||
|
- DeepSeek ([Get key](https://platform.deepseek.com/api_keys))
|
||||||
- OpenRouter ([Get key](https://openrouter.ai/keys))
|
- OpenRouter ([Get key](https://openrouter.ai/keys))
|
||||||
|
- Google Gemini 🆕 ([Get key](https://ai.google.dev/gemini-api/docs/api-key)) thanks to ([@Azzedde](https://github.com/Azzedde))
|
||||||
|
- Any OpenAI-compatible API provider
|
||||||
- Python 3.9 or newer
|
- Python 3.9 or newer
|
||||||
- Stable internet connection
|
- Stable internet connection
|
||||||
|
|
||||||
### Configuration Options
|
## 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
|
### 🔧 **Core Configuration Settings**
|
||||||
|
- 🌐 **API Provider**: OpenAI/Anthropic/DeepSeek/Gemini
|
||||||
|
- 🔑 **API Key**: Provider-specific authentication
|
||||||
|
- 🤖 **Model Selection**: Flexible, provider-specific models
|
||||||
|
- 🌡️ **Temperature**: Creativity control (0.0-2.0)
|
||||||
|
- 📏 **Max Tokens**: Response length limit (passed directly to the LLM API to control the maximum length of the response)
|
||||||
|
- ⏱️ **Request Interval**: API call throttling
|
||||||
|
- 💾 **History Size**: Number of messages to retain
|
||||||
|
- 🌍 **Custom API Endpoint**: Optional advanced configuration
|
||||||
|
|
||||||
|
### 🤖 **Recommended Models**
|
||||||
|
|
||||||
|
#### OpenAI Models
|
||||||
|
- **GPT-5** - The latest flagship model, best for complex reasoning
|
||||||
|
- **GPT-5 mini** - A cost-effective and fast model, suitable for most tasks
|
||||||
|
|
||||||
|
#### Anthropic Claude Models
|
||||||
|
- **Claude Opus 4.1** - The most capable model for handling complex tasks
|
||||||
|
- **Claude Sonnet 4** - Offers a balance between performance and cost
|
||||||
|
- **Claude Haiku 4** - The fastest and most economical option in the series
|
||||||
|
|
||||||
|
#### DeepSeek Models
|
||||||
|
- **DeepSeek-V3.1** - A general-purpose model for a wide range of tasks
|
||||||
|
- **DeepSeek-R1** - A specialized model focused on reasoning and coding
|
||||||
|
|
||||||
|
#### Google Gemini Models
|
||||||
|
- **Gemini 2.5 Pro & 2.5 Flash** - The newest and most advanced models available
|
||||||
|
- **Gemini 2.0 Pro & 2.0 Flash** - Previous generation models that are still powerful and efficient
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🌐 Potentially Compatible Providers</summary>
|
||||||
|
|
||||||
|
#### Flexible Provider Ecosystem
|
||||||
The integration is designed to be flexible and may work with other providers offering OpenAI-compatible APIs:
|
The integration is designed to be flexible and may work with other providers offering OpenAI-compatible APIs:
|
||||||
- Groq
|
- Groq
|
||||||
- Together AI
|
- Together AI
|
||||||
@@ -104,31 +161,41 @@ The integration is designed to be flexible and may work with other providers off
|
|||||||
- Local AI servers (like Ollama)
|
- Local AI servers (like Ollama)
|
||||||
- Custom OpenAI-compatible endpoints
|
- Custom OpenAI-compatible endpoints
|
||||||
|
|
||||||
#### Additional Notes
|
#### 🚨 Compatibility Notes
|
||||||
- Not all providers guarantee full compatibility
|
- Not all providers guarantee full compatibility
|
||||||
- Performance may vary between providers
|
- Performance may vary between providers
|
||||||
- Check individual provider's documentation
|
- Check individual provider's documentation
|
||||||
- Ensure your API key has sufficient credits/quota
|
- Ensure your API key has sufficient credits/quota
|
||||||
|
|
||||||
#### Provider Compatibility Requirements
|
#### 🔍 Provider Compatibility Requirements
|
||||||
To be compatible, a provider should support:
|
To be compatible, a provider should support:
|
||||||
- OpenAI-like REST API structure
|
- OpenAI-like REST API structure
|
||||||
- JSON request/response format
|
- JSON request/response format
|
||||||
- Standard authentication method
|
- Standard authentication method
|
||||||
- Similar model parameter handling
|
- Similar model parameter handling
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## ⚡ Installation
|
## ⚡ Installation
|
||||||
|
|
||||||
### HACS Installation (Recommended)
|
### HACS Installation (Recommended)
|
||||||
|
>[!TIP]
|
||||||
|
>HA Text AI is available in the default HACS repository. You can install it directly through HACS or click the button below to open it there.
|
||||||
|
|
||||||
<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>
|
<a href="https://my.home-assistant.io/redirect/hacs_repository/?owner=smkrv&repository=ha-text-ai&category=Integration"><img src="https://my.home-assistant.io/badges/hacs_repository.svg" width="170" height="auto"></a>
|
||||||
1. Open HACS in Home Assistant
|
1. Open HACS in Home Assistant
|
||||||
2. Click on "Integrations"
|
2. Click on "Integrations"
|
||||||
3. Click "..." in top right corner
|
3. Search for "HA Text AI"
|
||||||
4. Select "Custom repositories"
|
4. Click "Download"
|
||||||
5. Add repository URL: `https://github.com/smkrv/ha-text-ai`
|
5. Restart Home Assistant
|
||||||
6. Choose "Integration" as category
|
|
||||||
7. Click "Download"
|
**Alternative Method (Custom Repository):**
|
||||||
8. Restart Home Assistant
|
If the integration is not found in the default repository:
|
||||||
|
1. Click "..." in top right corner of HACS
|
||||||
|
2. Select "Custom repositories"
|
||||||
|
3. Add repository URL: `https://github.com/smkrv/ha-text-ai`
|
||||||
|
4. Choose "Integration" as category
|
||||||
|
5. Click "Download"
|
||||||
|
|
||||||
### Manual Installation
|
### Manual Installation
|
||||||
1. Download the latest release
|
1. Download the latest release
|
||||||
@@ -144,7 +211,8 @@ To be compatible, a provider should support:
|
|||||||
3. Search for "HA Text AI"
|
3. Search for "HA Text AI"
|
||||||
4. Follow the configuration steps
|
4. Follow the configuration steps
|
||||||
|
|
||||||
### Via YAML
|
<details>
|
||||||
|
<summary>📦 Via YAML (Advanced)</summary>
|
||||||
|
|
||||||
### Platform Configuration (Global Settings)
|
### Platform Configuration (Global Settings)
|
||||||
|
|
||||||
@@ -152,7 +220,7 @@ To be compatible, a provider should support:
|
|||||||
ha_text_ai:
|
ha_text_ai:
|
||||||
api_provider: openai # Required
|
api_provider: openai # Required
|
||||||
api_key: !secret ai_api_key # Required
|
api_key: !secret ai_api_key # Required
|
||||||
model: gpt-4o-mini # Strongly recommended
|
model: gpt-4o # Strongly recommended
|
||||||
temperature: 0.7 # Optional
|
temperature: 0.7 # Optional
|
||||||
max_tokens: 1000 # Optional
|
max_tokens: 1000 # Optional
|
||||||
request_interval: 1.0 # Optional
|
request_interval: 1.0 # Optional
|
||||||
@@ -169,7 +237,7 @@ sensor:
|
|||||||
- platform: ha_text_ai
|
- platform: ha_text_ai
|
||||||
name: "My AI Assistant" # Required, unique identifier
|
name: "My AI Assistant" # Required, unique identifier
|
||||||
api_provider: openai # Optional (inherits from platform)
|
api_provider: openai # Optional (inherits from platform)
|
||||||
model: "gpt-4o-mini" # Optional
|
model: "gpt-4o" # Optional
|
||||||
temperature: 0.7 # Optional
|
temperature: 0.7 # Optional
|
||||||
max_tokens: 1000 # Optional
|
max_tokens: 1000 # Optional
|
||||||
```
|
```
|
||||||
@@ -180,16 +248,16 @@ sensor:
|
|||||||
|
|
||||||
| Parameter | Type | Required | Default | Description |
|
| Parameter | Type | Required | Default | Description |
|
||||||
|-----------|------|----------|---------|-------------|
|
|-----------|------|----------|---------|-------------|
|
||||||
| `api_provider` | String | ✅ | - | AI service provider (openai, anthropic) |
|
| `api_provider` | String | ✅ | - | AI service provider (openai, anthropic, deepseek, gemini) |
|
||||||
| `api_key` | String | ✅ | - | Authentication key for AI service |
|
| `api_key` | String | ✅ | - | Authentication key for AI service |
|
||||||
| `model` | String | ⚠️ | Provider default | Strongly recommended: Specific AI model to use. If not specified, the provider's default model will be used |
|
| `model` | String | ⚠️ | gpt-4o-mini | Strongly recommended: Specific AI model to use. Default varies by provider |
|
||||||
| `temperature` | Float | ❌ | 0.7 | Response creativity level (0.0-2.0) |
|
| `temperature` | Float | ❌ | 0.1 | Response creativity level (0.0-2.0) |
|
||||||
| `max_tokens` | Integer | ❌ | 1000 | Maximum response length |
|
| `max_tokens` | Integer | ❌ | 1000 | Maximum response length |
|
||||||
| `request_interval` | Float | ❌ | 1.0 | Delay between API requests |
|
| `request_interval` | Float | ❌ | 1.0 | Delay between API requests |
|
||||||
| `api_endpoint` | URL | ⚠️ | Provider default | Custom API endpoint |
|
| `api_endpoint` | URL | ⚠️ | Provider default | Custom API endpoint |
|
||||||
| `system_prompt` | String | ❌ | - | Default context for AI interactions |
|
| `system_prompt` | String | ❌ | - | Default context for AI interactions |
|
||||||
| `max_history_size` | Integer | ❌ | 100 | Maximum number of conversation entries to store |
|
| `max_history_size` | Integer | ❌ | 50 | Maximum number of conversation entries to store |
|
||||||
| `history_file_size` | Integer | ⚠️ | 1 | Maximum history file size in MB |
|
| `context_messages` | Integer | ❌ | 5 | Number of previous messages to include in context (1-20) |
|
||||||
|
|
||||||
#### Sensor Configuration
|
#### Sensor Configuration
|
||||||
|
|
||||||
@@ -198,28 +266,58 @@ sensor:
|
|||||||
| `platform` | String | ✅ | - | Must be `ha_text_ai` |
|
| `platform` | String | ✅ | - | Must be `ha_text_ai` |
|
||||||
| `name` | String | ✅ | - | Unique sensor identifier |
|
| `name` | String | ✅ | - | Unique sensor identifier |
|
||||||
| `api_provider` | String | ❌ | Platform setting | Override global provider |
|
| `api_provider` | String | ❌ | Platform setting | Override global provider |
|
||||||
| `model` | String | ⚠️ | Platform setting | Recommended: Override global model. If not specified, uses platform or provider default |
|
| `model` | String | ⚠️ | Provider default | Recommended: Override global model (gpt-4o-mini, deepseek-chat, gemini-2.0-flash) |
|
||||||
| `temperature` | Float | ❌ | Platform setting | Override global temperature |
|
| `temperature` | Float | ❌ | 0.1 | Override global temperature |
|
||||||
| `max_tokens` | Integer | ❌ | Platform setting | Override global max tokens |
|
| `max_tokens` | Integer | ❌ | 1000 | Override global max tokens |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## 🛠️ Available Services
|
## 🛠️ Available Services
|
||||||
|
|
||||||
|
### 🔄 Response Variables (New!)
|
||||||
|
|
||||||
|
**HA Text AI now supports response variables** - a powerful feature that returns AI responses directly from service calls, eliminating the need for separate text sensors and the 255-character limitation!
|
||||||
|
|
||||||
|
#### ✨ Key Benefits:
|
||||||
|
- **Unlimited response length** - No more 255-character truncation
|
||||||
|
- **Direct data access** - Get responses immediately in automations
|
||||||
|
- **Race condition prevention** - Eliminates conflicts in parallel automations
|
||||||
|
- **Simplified workflows** - No need to read from sensors
|
||||||
|
|
||||||
### ask_question
|
### ask_question
|
||||||
```yaml
|
```yaml
|
||||||
service: ha_text_ai.ask_question
|
service: ha_text_ai.ask_question
|
||||||
data:
|
data:
|
||||||
question: "What's the optimal temperature for sleeping?"
|
question: "What's the optimal temperature for sleeping?"
|
||||||
model: "claude-3-sonnet" # optional
|
model: "claude-3.5-sonnet" # optional
|
||||||
temperature: 0.5 # optional
|
temperature: 0.5 # optional
|
||||||
max_tokens: 500 # optional
|
max_tokens: 500 # optional
|
||||||
context_messages: 10 #optional, number of previous messages to include in context, default: 5
|
context_messages: 10 #optional, number of previous messages to include in context, default: 5
|
||||||
system_prompt: "You are a sleep optimization expert" # optional
|
system_prompt: "You are a sleep optimization expert" # optional
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: ai_response # NEW! Store response data directly
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 📊 Response Data Structure:
|
||||||
|
```yaml
|
||||||
|
# The service returns structured data:
|
||||||
|
response_text: "The optimal sleeping temperature is 65-68°F (18-20°C)..."
|
||||||
|
tokens_used: 150
|
||||||
|
prompt_tokens: 50
|
||||||
|
completion_tokens: 100
|
||||||
|
model_used: "claude-3.5-sonnet"
|
||||||
|
instance: "sensor.ha_text_ai_gpt"
|
||||||
|
question: "What's the optimal temperature for sleeping?"
|
||||||
|
timestamp: "2025-02-09T16:57:00.000Z"
|
||||||
|
success: true
|
||||||
|
# error: "Error message" (only present if success: false)
|
||||||
```
|
```
|
||||||
|
|
||||||
### set_system_prompt
|
### set_system_prompt
|
||||||
```yaml
|
```yaml
|
||||||
service: ha_text_ai.set_system_prompt
|
service: ha_text_ai.set_system_prompt
|
||||||
data:
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
prompt: |
|
prompt: |
|
||||||
You are a home automation expert focused on:
|
You are a home automation expert focused on:
|
||||||
1. Energy efficiency
|
1. Energy efficiency
|
||||||
@@ -231,14 +329,163 @@ data:
|
|||||||
### clear_history
|
### clear_history
|
||||||
```yaml
|
```yaml
|
||||||
service: ha_text_ai.clear_history
|
service: ha_text_ai.clear_history
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
```
|
```
|
||||||
|
|
||||||
### get_history
|
### get_history
|
||||||
```yaml
|
```yaml
|
||||||
service: ha_text_ai.get_history
|
service: ha_text_ai.get_history
|
||||||
data:
|
data:
|
||||||
limit: 5 # optional
|
limit: 5 # optional, number of conversations to return (1-100)
|
||||||
filter_model: "gpt-4o" # optional
|
filter_model: "gpt-4o" # optional, filter by specific AI model
|
||||||
|
start_date: "2025-02-01" # optional, filter conversations from this date
|
||||||
|
include_metadata: false # optional, include tokens, response time, etc.
|
||||||
|
sort_order: "newest" # optional, sort order: "newest" or "oldest"
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Advanced Automation Examples with Response Variables
|
||||||
|
|
||||||
|
### Example 1: Smart Home Advice with Direct Response
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Get AI Home Advice"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: input_button.ask_ai_advice
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: "What's the best way to optimize energy usage in my home?"
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: ai_advice
|
||||||
|
- service: notify.mobile_app
|
||||||
|
data:
|
||||||
|
title: "🏠 Smart Home Tip"
|
||||||
|
message: |
|
||||||
|
{{ ai_advice.response_text }}
|
||||||
|
|
||||||
|
📊 Tokens used: {{ ai_advice.tokens_used }}
|
||||||
|
🤖 Model: {{ ai_advice.model_used }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Weather-Based AI Recommendations
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Weather-Based AI Suggestions"
|
||||||
|
trigger:
|
||||||
|
- platform: numeric_state
|
||||||
|
entity_id: sensor.outdoor_temperature
|
||||||
|
below: 0
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: |
|
||||||
|
The outdoor temperature is {{ states('sensor.outdoor_temperature') }}°C.
|
||||||
|
What should I do to prepare my home for freezing weather?
|
||||||
|
system_prompt: "You are a home maintenance expert. Provide practical, actionable advice."
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: winter_advice
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ winter_advice.success }}"
|
||||||
|
then:
|
||||||
|
- service: persistent_notification.create
|
||||||
|
data:
|
||||||
|
title: "❄️ Winter Preparation Advice"
|
||||||
|
message: |
|
||||||
|
{{ winter_advice.response_text }}
|
||||||
|
|
||||||
|
Generated at: {{ winter_advice.timestamp }}
|
||||||
|
else:
|
||||||
|
- service: persistent_notification.create
|
||||||
|
data:
|
||||||
|
title: "⚠️ AI Service Error"
|
||||||
|
message: "Failed to get winter advice: {{ winter_advice.error }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Multi-Step AI Workflow
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Multi-Step AI Analysis"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: input_button.analyze_home_status
|
||||||
|
action:
|
||||||
|
# Step 1: Get current status analysis
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: |
|
||||||
|
Current home status:
|
||||||
|
- Temperature: {{ states('sensor.indoor_temperature') }}°C
|
||||||
|
- Humidity: {{ states('sensor.indoor_humidity') }}%
|
||||||
|
- Energy usage: {{ states('sensor.power_consumption') }}W
|
||||||
|
|
||||||
|
Analyze this data and provide insights.
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: status_analysis
|
||||||
|
|
||||||
|
# Step 2: Get recommendations based on analysis
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: |
|
||||||
|
Based on this analysis: "{{ status_analysis.response_text[:500] }}"
|
||||||
|
|
||||||
|
Provide 3 specific actionable recommendations for improvement.
|
||||||
|
context_messages: 2 # Include previous conversation
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: recommendations
|
||||||
|
|
||||||
|
# Step 3: Send comprehensive report
|
||||||
|
- service: notify.telegram
|
||||||
|
data:
|
||||||
|
title: "🏠 Home Analysis Report"
|
||||||
|
message: |
|
||||||
|
**Analysis:**
|
||||||
|
{{ status_analysis.response_text }}
|
||||||
|
|
||||||
|
**Recommendations:**
|
||||||
|
{{ recommendations.response_text }}
|
||||||
|
|
||||||
|
**Report Details:**
|
||||||
|
- Total tokens used: {{ status_analysis.tokens_used + recommendations.tokens_used }}
|
||||||
|
- Analysis model: {{ status_analysis.model_used }}
|
||||||
|
- Generated: {{ recommendations.timestamp }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💡 Migration from Sensors to Response Variables
|
||||||
|
|
||||||
|
#### Old Method (Limited):
|
||||||
|
```yaml
|
||||||
|
# ❌ Old way - limited to 255 characters, race conditions
|
||||||
|
automation:
|
||||||
|
- alias: "Old AI Response Method"
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: "Long question here..."
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
- delay: "00:00:05" # Wait for sensor update
|
||||||
|
- service: notify.mobile
|
||||||
|
data:
|
||||||
|
message: "{{ state_attr('sensor.ha_text_ai_gpt', 'response')[:255] }}..." # Truncated!
|
||||||
|
```
|
||||||
|
|
||||||
|
#### New Method (Unlimited):
|
||||||
|
```yaml
|
||||||
|
# ✅ New way - unlimited length, immediate access, no race conditions
|
||||||
|
automation:
|
||||||
|
- alias: "New AI Response Method"
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: "Long question here..."
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: ai_response # Direct access!
|
||||||
|
- service: notify.mobile
|
||||||
|
data:
|
||||||
|
message: "{{ ai_response.response_text }}" # Full response, no truncation!
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🏷️ HA Text AI Sensor Naming Convention
|
### 🏷️ HA Text AI Sensor Naming Convention
|
||||||
@@ -258,7 +505,7 @@ sensor.ha_text_ai_YOUR_UNIQUE_SUFFIX
|
|||||||
# Examples:
|
# Examples:
|
||||||
sensor.ha_text_ai_gpt # GPT-based sensor
|
sensor.ha_text_ai_gpt # GPT-based sensor
|
||||||
sensor.ha_text_ai_claude # Claude-based sensor
|
sensor.ha_text_ai_claude # Claude-based sensor
|
||||||
sensor.ha_text_ai_gpt # Custom suffix
|
sensor.ha_text_ai_abc # Custom suffix
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Response Retrieval
|
#### Response Retrieval
|
||||||
@@ -275,6 +522,7 @@ automation:
|
|||||||
- service: ha_text_ai.ask_question
|
- service: ha_text_ai.ask_question
|
||||||
data:
|
data:
|
||||||
question: "Home automation advice"
|
question: "Home automation advice"
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
- service: notify.mobile
|
- service: notify.mobile
|
||||||
data:
|
data:
|
||||||
message: >
|
message: >
|
||||||
@@ -291,6 +539,16 @@ automation:
|
|||||||
|
|
||||||
### 🔍 HA Text AI Sensor Attributes
|
### 🔍 HA Text AI Sensor Attributes
|
||||||
|
|
||||||
|
- 🤖 **Model and Provider Information**: Tracking current AI model and service provider
|
||||||
|
- 🚦 **System Status**: Real-time API and processing readiness
|
||||||
|
- 📊 **Performance Metrics**: Request success rates and response times
|
||||||
|
- 💬 **Conversation Tracking**: Token usage and interaction history are estimated using a heuristic method based on word count and specific word characteristics, which may differ from actual token usage.
|
||||||
|
- 🕒 **Last Interaction Details**: Recent query and response tracking
|
||||||
|
- ❤️ **System Health**: Error monitoring and service uptime
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>📦 Detailed Sensor Attributes</summary>
|
||||||
|
|
||||||
#### Model and Provider Information
|
#### Model and Provider Information
|
||||||
```yaml
|
```yaml
|
||||||
# Name of the AI model currently in use (e.g., latest version of GPT)
|
# Name of the AI model currently in use (e.g., latest version of GPT)
|
||||||
@@ -350,9 +608,8 @@ automation:
|
|||||||
# Number of entries in current history file
|
# Number of entries in current history file
|
||||||
{{ state_attr('sensor.ha_text_ai_gpt', 'History size') }} # 0
|
{{ state_attr('sensor.ha_text_ai_gpt', 'History size') }} # 0
|
||||||
|
|
||||||
# Last few conversation entries (limited to 3 for performance)
|
# Last few conversation entries (limited to 1 for performance)
|
||||||
{{ state_attr('sensor.ha_text_ai_gpt', 'conversation_history') }} # [...]
|
{{ state_attr('sensor.ha_text_ai_gpt', 'conversation_history') }} # [...]
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Last Interaction Details
|
#### Last Interaction Details
|
||||||
@@ -381,7 +638,7 @@ automation:
|
|||||||
|
|
||||||
### History Storage
|
### History Storage
|
||||||
Conversation history stored in `.storage/ha_text_ai_history/` directory:
|
Conversation history stored in `.storage/ha_text_ai_history/` directory:
|
||||||
- Each instance has its own history file
|
- Each instance has its own history file (JSON)
|
||||||
- Files are automatically rotated when size limit is reached
|
- Files are automatically rotated when size limit is reached
|
||||||
- Archived history files are timestamped
|
- Archived history files are timestamped
|
||||||
- Default maximum file size: 1MB
|
- Default maximum file size: 1MB
|
||||||
@@ -391,14 +648,15 @@ Conversation history stored in `.storage/ha_text_ai_history/` directory:
|
|||||||
- Use these attributes for monitoring and automation
|
- Use these attributes for monitoring and automation
|
||||||
- Some values might be 0 or empty initially
|
- Some values might be 0 or empty initially
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## 📘 FAQ
|
## 📘 FAQ
|
||||||
|
|
||||||
**Q: Which AI providers are supported?**
|
**Q: Which AI providers are supported?**
|
||||||
A: Currently OpenAI (GPT models) and Anthropic (Claude models) are supported, with more providers planned.
|
A: OpenAI (GPT models), Anthropic (Claude models), DeepSeek, Google Gemini, and OpenRouter are officially supported, with many other OpenAI-compatible providers working as well.
|
||||||
|
|
||||||
**Q: How can I reduce API costs?**
|
**Q: How can I reduce API costs?**
|
||||||
A: Use GPT-3.5-Turbo or Claude-3-Sonnet for most queries, implement caching, and optimize token usage.
|
A: Use gpt-4o-mini or claude-3.5-haiku for most queries, implement caching, and optimize token usage.
|
||||||
|
|
||||||
**Q: Are there limitations on the number of requests?**
|
**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.
|
A: Depends on your API provider's plan. We recommend monitoring usage and implementing request throttling via `request_interval` configuration.
|
||||||
@@ -409,8 +667,11 @@ A: Yes, you can configure custom endpoints and use any compatible model by speci
|
|||||||
**Q: How do I switch between different AI providers?**
|
**Q: How do I switch between different AI providers?**
|
||||||
A: Simply change the model parameter in your configuration or service calls to use the desired provider's model.
|
A: Simply change the model parameter in your configuration or service calls to use the desired provider's model.
|
||||||
|
|
||||||
**Q: How can I reduce API costs?**
|
**Q: What are the token limits for different models?**
|
||||||
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage.
|
A: Token limits vary by provider and model. OpenAI's gpt-4o supports up to 128K tokens, Claude 3.5 Sonnet supports up to 200K tokens, while smaller models typically have 8K-32K limits. Check your provider's documentation for specific limits.
|
||||||
|
|
||||||
|
**Q: How do I monitor token usage?**
|
||||||
|
A: Use the sensor attributes like `Total tokens`, `Prompt tokens`, and `Completion tokens` to track usage. You can also create automations to alert you when usage exceeds certain thresholds.
|
||||||
|
|
||||||
**Q: Is my data secure?**
|
**Q: Is my data secure?**
|
||||||
A: Yes, your data is secure. The system operates entirely on your local machine, keeping your data under your control. API keys are stored securely and all external communications use encrypted connections.
|
A: Yes, 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.
|
||||||
@@ -427,7 +688,6 @@ A: Yes, archived history files are stored with timestamps and can be accessed ma
|
|||||||
**Q: How much history is kept?**
|
**Q: How much history is kept?**
|
||||||
A: By default, up to 100 conversations are stored, but this can be configured. Files are automatically rotated when they reach 1MB.
|
A: By default, up to 100 conversations are stored, but this can be configured. Files are automatically rotated when they reach 1MB.
|
||||||
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
|
Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
|
||||||
@@ -474,9 +734,10 @@ If you want to say thanks financially, you can send a small token of appreciatio
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<div align="center"><img src="https://github.com/smkrv/ha-text-ai/blob/2aaf3405759eb2d97624834594e24ace896131df/assets/images/icons/footer_icon.png" alt="HA Text AI" style="width: 128px; height: auto;"/></div>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
Made with ❤️ and Claude 3.5 Sonnet for the Home Assistant Community
|
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)
|
[Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Using response_variable with HA Text AI
|
||||||
|
|
||||||
|
After updating the HA Text AI integration, it now supports using the `response_variable` parameter in Home Assistant scripts and automations.
|
||||||
|
|
||||||
|
## What Changed
|
||||||
|
|
||||||
|
- Added response schema support in the `ha_text_ai.ask_question` service
|
||||||
|
- Service is now correctly registered with `supports_response=True` flag
|
||||||
|
- You can now use `response_variable` to capture AI response in a variable
|
||||||
|
|
||||||
|
## Example Usage in Script
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
context_messages: 0
|
||||||
|
temperature: 0.7
|
||||||
|
max_tokens: 1000
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "What time is it?"
|
||||||
|
response_variable: ai_response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Usage in Automation
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alias: "Get AI Response"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: input_boolean.ask_ai
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "What's the current weather?"
|
||||||
|
temperature: 0.7
|
||||||
|
max_tokens: 500
|
||||||
|
response_variable: weather_response
|
||||||
|
|
||||||
|
- action: notify.persistent_notification
|
||||||
|
data:
|
||||||
|
title: "AI Response"
|
||||||
|
message: "{{ weather_response.response_text }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Fields in response_variable
|
||||||
|
|
||||||
|
When you use `response_variable`, you will receive an object with the following fields:
|
||||||
|
|
||||||
|
- `response_text` (string) - The AI response text
|
||||||
|
- `tokens_used` (integer) - Total number of tokens used
|
||||||
|
- `prompt_tokens` (integer) - Number of tokens in the prompt
|
||||||
|
- `completion_tokens` (integer) - Number of tokens in the completion
|
||||||
|
- `model_used` (string) - The AI model that was used for the response
|
||||||
|
- `instance` (string) - The instance name that was used
|
||||||
|
- `question` (string) - The original question that was asked
|
||||||
|
- `timestamp` (string) - ISO timestamp when the response was generated
|
||||||
|
- `success` (boolean) - Whether the request was successful
|
||||||
|
- `error` (string) - Error message if the request failed
|
||||||
|
|
||||||
|
## Example Using Response Fields
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action:
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Tell me a joke"
|
||||||
|
response_variable: joke_response
|
||||||
|
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ joke_response.success }}"
|
||||||
|
|
||||||
|
- action: input_text.set_value
|
||||||
|
target:
|
||||||
|
entity_id: input_text.last_ai_response
|
||||||
|
data:
|
||||||
|
value: "{{ joke_response.response_text }}"
|
||||||
|
|
||||||
|
- action: input_number.set_value
|
||||||
|
target:
|
||||||
|
entity_id: input_number.tokens_used
|
||||||
|
data:
|
||||||
|
value: "{{ joke_response.tokens_used }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action:
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Test question"
|
||||||
|
response_variable: ai_result
|
||||||
|
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ ai_result.success }}"
|
||||||
|
sequence:
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
title: "AI Response"
|
||||||
|
message: "{{ ai_result.response_text }}"
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ not ai_result.success }}"
|
||||||
|
sequence:
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
title: "AI Error"
|
||||||
|
message: "Error: {{ ai_result.error }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Old Approach
|
||||||
|
|
||||||
|
**Old method (without response_variable):**
|
||||||
|
```yaml
|
||||||
|
# Ask question
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Hello!"
|
||||||
|
|
||||||
|
# Wait and read response from sensor
|
||||||
|
- delay: 00:00:05
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
message: "{{ states('sensor.ha_text_ai_gemini') }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**New method (with response_variable):**
|
||||||
|
```yaml
|
||||||
|
# Ask question and get response immediately
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Hello!"
|
||||||
|
response_variable: greeting_response
|
||||||
|
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
message: "{{ greeting_response.response_text }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
The new approach is more reliable as it doesn't require waiting and reading from the sensor.
|
||||||
|
After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 618 KiB After Width: | Height: | Size: 618 KiB |
|
Before Width: | Height: | Size: 923 KiB After Width: | Height: | Size: 923 KiB |
|
After Width: | Height: | Size: 339 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
@@ -11,8 +11,9 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import hashlib
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, TypeVar
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from async_timeout import timeout
|
from async_timeout import timeout
|
||||||
@@ -38,11 +39,17 @@ from .const import (
|
|||||||
CONF_CONTEXT_MESSAGES,
|
CONF_CONTEXT_MESSAGES,
|
||||||
API_PROVIDER_OPENAI,
|
API_PROVIDER_OPENAI,
|
||||||
API_PROVIDER_ANTHROPIC,
|
API_PROVIDER_ANTHROPIC,
|
||||||
|
API_PROVIDER_DEEPSEEK,
|
||||||
|
API_PROVIDER_GEMINI,
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
|
DEFAULT_DEEPSEEK_MODEL,
|
||||||
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_TEMPERATURE,
|
DEFAULT_TEMPERATURE,
|
||||||
DEFAULT_MAX_TOKENS,
|
DEFAULT_MAX_TOKENS,
|
||||||
DEFAULT_OPENAI_ENDPOINT,
|
DEFAULT_OPENAI_ENDPOINT,
|
||||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||||
|
DEFAULT_DEEPSEEK_ENDPOINT,
|
||||||
|
DEFAULT_GEMINI_ENDPOINT,
|
||||||
DEFAULT_REQUEST_INTERVAL,
|
DEFAULT_REQUEST_INTERVAL,
|
||||||
DEFAULT_CONTEXT_MESSAGES,
|
DEFAULT_CONTEXT_MESSAGES,
|
||||||
API_TIMEOUT,
|
API_TIMEOUT,
|
||||||
@@ -52,11 +59,13 @@ from .const import (
|
|||||||
SERVICE_SET_SYSTEM_PROMPT,
|
SERVICE_SET_SYSTEM_PROMPT,
|
||||||
DEFAULT_MAX_HISTORY,
|
DEFAULT_MAX_HISTORY,
|
||||||
CONF_MAX_HISTORY_SIZE,
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
ICONS_SUBDOMAIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
ConfigType = TypeVar("ConfigType", bound=Dict[str, Any])
|
||||||
|
|
||||||
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||||
vol.Required("instance"): cv.string,
|
vol.Required("instance"): cv.string,
|
||||||
@@ -77,6 +86,9 @@ SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
|||||||
vol.Required("instance"): cv.string,
|
vol.Required("instance"): cv.string,
|
||||||
vol.Optional("limit"): cv.positive_int,
|
vol.Optional("limit"): cv.positive_int,
|
||||||
vol.Optional("filter_model"): cv.string,
|
vol.Optional("filter_model"): cv.string,
|
||||||
|
vol.Optional("start_date"): cv.string,
|
||||||
|
vol.Optional("include_metadata"): cv.boolean,
|
||||||
|
vol.Optional("sort_order"): vol.In(["newest", "oldest"]),
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator:
|
def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator:
|
||||||
@@ -90,25 +102,24 @@ def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAIC
|
|||||||
|
|
||||||
raise HomeAssistantError(f"Instance {instance} not found")
|
raise HomeAssistantError(f"Instance {instance} not found")
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
def get_file_hash(file_path: str) -> str:
|
||||||
"""Set up the HA Text AI component."""
|
"""Calculate SHA256 hash of file."""
|
||||||
|
sha256_hash = hashlib.sha256()
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
for byte_block in iter(lambda: f.read(4096), b""):
|
||||||
|
sha256_hash.update(byte_block)
|
||||||
|
return sha256_hash.hexdigest()
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up the Home Assistant Text AI component."""
|
||||||
|
# Initialize domain data storage
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
try:
|
async def async_ask_question(call: ServiceCall) -> dict:
|
||||||
source = os.path.join(os.path.dirname(__file__), 'icons', 'icon.svg')
|
"""Handle ask_question service with response data."""
|
||||||
dest_dir = os.path.join(hass.config.path('www'), 'icons')
|
|
||||||
os.makedirs(dest_dir, exist_ok=True)
|
|
||||||
dest = os.path.join(dest_dir, 'icon.png')
|
|
||||||
if not os.path.exists(dest):
|
|
||||||
shutil.copyfile(source, dest)
|
|
||||||
except Exception as ex:
|
|
||||||
_LOGGER.warning("Failed to copy custom icon: %s", str(ex))
|
|
||||||
|
|
||||||
async def async_ask_question(call: ServiceCall) -> None:
|
|
||||||
"""Handle ask_question service."""
|
|
||||||
try:
|
try:
|
||||||
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
await coordinator.async_ask_question(
|
response = await coordinator.async_ask_question(
|
||||||
question=call.data["question"],
|
question=call.data["question"],
|
||||||
model=call.data.get("model"),
|
model=call.data.get("model"),
|
||||||
temperature=call.data.get("temperature"),
|
temperature=call.data.get("temperature"),
|
||||||
@@ -116,9 +127,34 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
|||||||
system_prompt=call.data.get("system_prompt"),
|
system_prompt=call.data.get("system_prompt"),
|
||||||
context_messages=call.data.get("context_messages"),
|
context_messages=call.data.get("context_messages"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Return structured response data
|
||||||
|
return {
|
||||||
|
"response_text": response.get("content", ""),
|
||||||
|
"tokens_used": response.get("tokens", {}).get("total", 0),
|
||||||
|
"prompt_tokens": response.get("tokens", {}).get("prompt", 0),
|
||||||
|
"completion_tokens": response.get("tokens", {}).get("completion", 0),
|
||||||
|
"model_used": response.get("model", call.data.get("model", coordinator.model)),
|
||||||
|
"instance": call.data["instance"],
|
||||||
|
"question": call.data["question"],
|
||||||
|
"timestamp": response.get("timestamp"),
|
||||||
|
"success": True
|
||||||
|
}
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error asking question: %s", str(err))
|
_LOGGER.error("Error asking question: %s", str(err))
|
||||||
raise HomeAssistantError(f"Failed to process question: {str(err)}")
|
# Return error response
|
||||||
|
return {
|
||||||
|
"response_text": "",
|
||||||
|
"tokens_used": 0,
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 0,
|
||||||
|
"model_used": call.data.get("model", ""),
|
||||||
|
"instance": call.data["instance"],
|
||||||
|
"question": call.data["question"],
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"success": False,
|
||||||
|
"error": str(err)
|
||||||
|
}
|
||||||
|
|
||||||
async def async_clear_history(call: ServiceCall) -> None:
|
async def async_clear_history(call: ServiceCall) -> None:
|
||||||
"""Handle clear_history service."""
|
"""Handle clear_history service."""
|
||||||
@@ -135,7 +171,10 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
|||||||
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
return await coordinator.async_get_history(
|
return await coordinator.async_get_history(
|
||||||
limit=call.data.get("limit"),
|
limit=call.data.get("limit"),
|
||||||
filter_model=call.data.get("filter_model")
|
filter_model=call.data.get("filter_model"),
|
||||||
|
start_date=call.data.get("start_date"),
|
||||||
|
include_metadata=call.data.get("include_metadata", False),
|
||||||
|
sort_order=call.data.get("sort_order", "newest")
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error getting history: %s", str(err))
|
_LOGGER.error("Error getting history: %s", str(err))
|
||||||
@@ -150,11 +189,13 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
|||||||
_LOGGER.error("Error setting system prompt: %s", str(err))
|
_LOGGER.error("Error setting system prompt: %s", str(err))
|
||||||
raise HomeAssistantError(f"Failed to set system prompt: {str(err)}")
|
raise HomeAssistantError(f"Failed to set system prompt: {str(err)}")
|
||||||
|
|
||||||
|
# Register services
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_ASK_QUESTION,
|
SERVICE_ASK_QUESTION,
|
||||||
async_ask_question,
|
async_ask_question,
|
||||||
schema=SERVICE_SCHEMA_ASK_QUESTION
|
schema=SERVICE_SCHEMA_ASK_QUESTION,
|
||||||
|
supports_response=True
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
@@ -178,13 +219,69 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
|||||||
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
|
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Handle icons
|
||||||
|
try:
|
||||||
|
source_icon_path = os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
ICONS_SUBDOMAIN,
|
||||||
|
'icon@2x.png'
|
||||||
|
)
|
||||||
|
|
||||||
|
destination_directory = os.path.join(
|
||||||
|
hass.config.path('www'),
|
||||||
|
DOMAIN,
|
||||||
|
ICONS_SUBDOMAIN
|
||||||
|
)
|
||||||
|
|
||||||
|
destination_icon_path = os.path.join(
|
||||||
|
destination_directory,
|
||||||
|
'icon.png'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(source_icon_path):
|
||||||
|
_LOGGER.error("Source icon not found: %s", source_icon_path)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def create_directory():
|
||||||
|
os.makedirs(destination_directory, exist_ok=True)
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(create_directory)
|
||||||
|
|
||||||
|
should_copy = True
|
||||||
|
|
||||||
|
if os.path.exists(destination_icon_path):
|
||||||
|
source_hash = await hass.async_add_executor_job(get_file_hash, source_icon_path)
|
||||||
|
dest_hash = await hass.async_add_executor_job(get_file_hash, destination_icon_path)
|
||||||
|
should_copy = source_hash != dest_hash
|
||||||
|
|
||||||
|
if should_copy:
|
||||||
|
def copy_file():
|
||||||
|
shutil.copyfile(source_icon_path, destination_icon_path)
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(copy_file)
|
||||||
|
_LOGGER.debug("Icon updated: %s", destination_icon_path)
|
||||||
|
|
||||||
|
except PermissionError as e:
|
||||||
|
_LOGGER.error("Permission denied when managing icons: %s", str(e))
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error("Failed to manage icons: %s", str(e))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
|
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
|
||||||
"""Check API availability for different providers."""
|
"""Check API availability for different providers."""
|
||||||
try:
|
try:
|
||||||
if provider == API_PROVIDER_ANTHROPIC:
|
if provider == API_PROVIDER_GEMINI:
|
||||||
|
# Gemini API does not support GET /models for validation, just check key presence
|
||||||
|
if headers.get("Authorization", "").replace("Bearer ", ""):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Gemini API key is missing or empty")
|
||||||
|
return False
|
||||||
|
elif provider == API_PROVIDER_ANTHROPIC:
|
||||||
check_url = f"{endpoint}/v1/models"
|
check_url = f"{endpoint}/v1/models"
|
||||||
|
elif provider == API_PROVIDER_DEEPSEEK:
|
||||||
|
check_url = f"{endpoint}/models"
|
||||||
else: # OpenAI
|
else: # OpenAI
|
||||||
check_url = f"{endpoint}/models"
|
check_url = f"{endpoint}/models"
|
||||||
|
|
||||||
@@ -193,7 +290,8 @@ async def async_check_api(session, endpoint: str, headers: dict, provider: str)
|
|||||||
if response.status in [200, 404]:
|
if response.status in [200, 404]:
|
||||||
return True
|
return True
|
||||||
elif response.status == 401:
|
elif response.status == 401:
|
||||||
raise ConfigEntryNotReady("Invalid API key")
|
_LOGGER.error("Invalid API key")
|
||||||
|
return False
|
||||||
elif response.status == 429:
|
elif response.status == 429:
|
||||||
_LOGGER.warning("Rate limit exceeded during API check")
|
_LOGGER.warning("Rate limit exceeded during API check")
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import asyncio
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
from aiohttp import ClientSession, ClientTimeout
|
from aiohttp import ClientSession, ClientTimeout
|
||||||
from async_timeout import timeout
|
from async_timeout import timeout
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
@@ -18,6 +19,9 @@ from .const import (
|
|||||||
API_TIMEOUT,
|
API_TIMEOUT,
|
||||||
API_RETRY_COUNT,
|
API_RETRY_COUNT,
|
||||||
API_PROVIDER_ANTHROPIC,
|
API_PROVIDER_ANTHROPIC,
|
||||||
|
API_PROVIDER_DEEPSEEK,
|
||||||
|
API_PROVIDER_OPENAI,
|
||||||
|
API_PROVIDER_GEMINI,
|
||||||
MIN_TEMPERATURE,
|
MIN_TEMPERATURE,
|
||||||
MAX_TEMPERATURE,
|
MAX_TEMPERATURE,
|
||||||
MIN_MAX_TOKENS,
|
MIN_MAX_TOKENS,
|
||||||
@@ -45,20 +49,36 @@ class APIClient:
|
|||||||
self.api_provider = api_provider
|
self.api_provider = api_provider
|
||||||
self.model = model
|
self.model = model
|
||||||
self.timeout = ClientTimeout(total=API_TIMEOUT)
|
self.timeout = ClientTimeout(total=API_TIMEOUT)
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
"""Async context manager entry."""
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Async context manager exit."""
|
||||||
|
await self.shutdown()
|
||||||
|
|
||||||
def _validate_parameters(
|
def _validate_parameters(
|
||||||
self,
|
self,
|
||||||
temperature: float,
|
temperature: float,
|
||||||
max_tokens: int,
|
max_tokens: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate API parameters."""
|
"""Validate API parameters with enhanced type checking."""
|
||||||
|
# Type validation
|
||||||
|
if not isinstance(temperature, (int, float)):
|
||||||
|
raise TypeError(f"Temperature must be a number, got {type(temperature)}")
|
||||||
|
if not isinstance(max_tokens, int):
|
||||||
|
raise TypeError(f"Max tokens must be an integer, got {type(max_tokens)}")
|
||||||
|
|
||||||
|
# Range validation
|
||||||
if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE:
|
if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}"
|
f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}, got {temperature}"
|
||||||
)
|
)
|
||||||
if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS:
|
if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}"
|
f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}, got {max_tokens}"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _make_request(
|
async def _make_request(
|
||||||
@@ -67,7 +87,10 @@ class APIClient:
|
|||||||
payload: Dict[str, Any],
|
payload: Dict[str, Any],
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Make API request with retry logic."""
|
"""Make API request with retry logic."""
|
||||||
_LOGGER.debug(f"API Request: URL={url}, Payload={payload}")
|
# Log request without sensitive data
|
||||||
|
safe_payload = {k: v for k, v in payload.items() if k not in ['messages', 'system']}
|
||||||
|
_LOGGER.debug(f"API Request: URL={url}, Safe payload: {safe_payload}")
|
||||||
|
|
||||||
for attempt in range(API_RETRY_COUNT):
|
for attempt in range(API_RETRY_COUNT):
|
||||||
try:
|
try:
|
||||||
async with timeout(API_TIMEOUT):
|
async with timeout(API_TIMEOUT):
|
||||||
@@ -80,16 +103,18 @@ class APIClient:
|
|||||||
_LOGGER.debug(f"Response status: {response.status}")
|
_LOGGER.debug(f"Response status: {response.status}")
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
error_data = await response.json()
|
error_data = await response.json()
|
||||||
_LOGGER.error(f"API error: {error_data}")
|
# Log error without sensitive data
|
||||||
raise HomeAssistantError(f"API error: {error_data}")
|
safe_error = {k: v for k, v in error_data.items() if k not in ['message', 'details']}
|
||||||
|
_LOGGER.error(f"API error (status {response.status}): {safe_error}")
|
||||||
|
raise HomeAssistantError(f"API error: status {response.status}")
|
||||||
return await response.json()
|
return await response.json()
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.warning(f"Timeout on attempt {attempt + 1}")
|
_LOGGER.warning(f"Timeout on attempt {attempt + 1}/{API_RETRY_COUNT}")
|
||||||
if attempt == API_RETRY_COUNT - 1:
|
if attempt == API_RETRY_COUNT - 1:
|
||||||
raise HomeAssistantError("API request timed out")
|
raise HomeAssistantError("API request timed out")
|
||||||
await asyncio.sleep(1 * (attempt + 1))
|
await asyncio.sleep(1 * (attempt + 1))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_LOGGER.warning(f"API request failed on attempt {attempt + 1}: {str(e)}")
|
_LOGGER.warning(f"API request failed on attempt {attempt + 1}/{API_RETRY_COUNT}: {type(e).__name__}")
|
||||||
if attempt == API_RETRY_COUNT - 1:
|
if attempt == API_RETRY_COUNT - 1:
|
||||||
raise
|
raise
|
||||||
await asyncio.sleep(1 * (attempt + 1))
|
await asyncio.sleep(1 * (attempt + 1))
|
||||||
@@ -109,19 +134,53 @@ class APIClient:
|
|||||||
return await self._create_anthropic_completion(
|
return await self._create_anthropic_completion(
|
||||||
model, messages, temperature, max_tokens
|
model, messages, temperature, max_tokens
|
||||||
)
|
)
|
||||||
|
elif self.api_provider == API_PROVIDER_DEEPSEEK:
|
||||||
|
return await self._create_deepseek_completion(
|
||||||
|
model, messages, temperature, max_tokens
|
||||||
|
)
|
||||||
|
elif self.api_provider == API_PROVIDER_GEMINI:
|
||||||
|
return await self._create_gemini_completion(
|
||||||
|
model, messages, temperature, max_tokens
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return await self._create_openai_completion(
|
return await self._create_openai_completion(
|
||||||
model, messages, temperature, max_tokens
|
model, messages, temperature, max_tokens
|
||||||
)
|
)
|
||||||
except (KeyError, IndexError) as e:
|
|
||||||
if "'choices'" in str(e) or "'message'" in str(e):
|
|
||||||
raise HomeAssistantError("Failed to get a response from the AI model. Please check your internet connection and try again later.")
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_LOGGER.error("API request failed: %s", str(e))
|
_LOGGER.error("API request failed: %s", str(e))
|
||||||
raise HomeAssistantError(f"API request failed: {str(e)}")
|
raise HomeAssistantError(f"API request failed: {str(e)}")
|
||||||
|
|
||||||
|
async def _create_deepseek_completion(
|
||||||
|
self,
|
||||||
|
model: str,
|
||||||
|
messages: List[Dict[str, str]],
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: int,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Create completion using DeepSeek API."""
|
||||||
|
url = f"{self.endpoint}/chat/completions"
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": messages,
|
||||||
|
"temperature": temperature,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
|
||||||
|
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_openai_completion(
|
async def _create_openai_completion(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
@@ -206,6 +265,143 @@ class APIClient:
|
|||||||
_LOGGER.error(f"Connection check failed: {str(e)}")
|
_LOGGER.error(f"Connection check failed: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def _create_gemini_completion(
|
||||||
|
self,
|
||||||
|
model: str,
|
||||||
|
messages: List[Dict[str, str]],
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: int,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Create completion using Gemini API with google-genai library.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: The model name to use
|
||||||
|
messages: List of message dictionaries with role and content
|
||||||
|
temperature: Sampling temperature between 0.0 and 2.0
|
||||||
|
max_tokens: Maximum number of tokens to generate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with response content and token usage
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
def import_genai():
|
||||||
|
from google import genai
|
||||||
|
return genai
|
||||||
|
|
||||||
|
genai = await asyncio.to_thread(import_genai)
|
||||||
|
|
||||||
|
# Extract API key from headers (Bearer token)
|
||||||
|
api_key = self.headers.get("Authorization", "").replace("Bearer ", "")
|
||||||
|
|
||||||
|
def create_client():
|
||||||
|
if self.endpoint and self.endpoint != "https://generativelanguage.googleapis.com/v1beta":
|
||||||
|
return genai.Client(api_key=api_key, transport="rest",
|
||||||
|
client_options={"api_endpoint": self.endpoint})
|
||||||
|
else:
|
||||||
|
return genai.Client(api_key=api_key)
|
||||||
|
|
||||||
|
client = await asyncio.to_thread(create_client)
|
||||||
|
|
||||||
|
# Process messages to extract system instruction and chat history
|
||||||
|
system_instruction = ""
|
||||||
|
contents = []
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
if msg['role'] == 'system':
|
||||||
|
system_instruction += msg['content'] + "\n"
|
||||||
|
else:
|
||||||
|
# For chat history, we need to convert to the format Gemini expects
|
||||||
|
role = "user" if msg['role'] == 'user' else "model"
|
||||||
|
contents.append({
|
||||||
|
"role": role,
|
||||||
|
"parts": [{"text": msg['content']}]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create configuration
|
||||||
|
def create_config():
|
||||||
|
from google.genai import types
|
||||||
|
config = types.GenerateContentConfig(
|
||||||
|
temperature=temperature,
|
||||||
|
max_output_tokens=max_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add system instruction if present
|
||||||
|
if system_instruction:
|
||||||
|
config.system_instruction = system_instruction.strip()
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
config = await asyncio.to_thread(create_config)
|
||||||
|
|
||||||
|
def generate_content():
|
||||||
|
# For single message without history, use generate_content
|
||||||
|
if len(contents) <= 1:
|
||||||
|
# If we have no content yet, create a simple prompt
|
||||||
|
if not contents:
|
||||||
|
prompt = "I need your assistance."
|
||||||
|
else:
|
||||||
|
prompt = contents[0]["parts"][0]["text"]
|
||||||
|
|
||||||
|
return client.models.generate_content(
|
||||||
|
model=model,
|
||||||
|
contents=prompt,
|
||||||
|
config=config
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# For multi-turn conversations, use chat
|
||||||
|
chat = client.chats.create(model=model, config=config)
|
||||||
|
|
||||||
|
# Send all messages in sequence
|
||||||
|
for content in contents:
|
||||||
|
if content["role"] == "user":
|
||||||
|
response = chat.send_message(content["parts"][0]["text"])
|
||||||
|
# We don't send assistant messages as they're already part of the history
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
response = await asyncio.to_thread(generate_content)
|
||||||
|
|
||||||
|
# Extract response text
|
||||||
|
def extract_response():
|
||||||
|
response_text = response.text if hasattr(response, 'text') else ""
|
||||||
|
|
||||||
|
# Try to get token usage if available
|
||||||
|
usage = {}
|
||||||
|
if hasattr(response, 'usage_metadata'):
|
||||||
|
usage = {
|
||||||
|
"prompt_tokens": getattr(response.usage_metadata, 'prompt_token_count', 0),
|
||||||
|
"completion_tokens": getattr(response.usage_metadata, 'candidates_token_count', 0),
|
||||||
|
"total_tokens": getattr(response.usage_metadata, 'total_token_count', 0)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Estimate token count as fallback
|
||||||
|
usage = {
|
||||||
|
"prompt_tokens": len(" ".join([m["content"] for m in messages]).split()) // 3,
|
||||||
|
"completion_tokens": len(response_text.split()) // 3,
|
||||||
|
"total_tokens": 0 # Will be calculated below
|
||||||
|
}
|
||||||
|
usage["total_tokens"] = usage["prompt_tokens"] + usage["completion_tokens"]
|
||||||
|
|
||||||
|
return response_text, usage
|
||||||
|
|
||||||
|
response_text, usage = await asyncio.to_thread(extract_response)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"choices": [{
|
||||||
|
"message": {
|
||||||
|
"content": response_text
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"usage": usage
|
||||||
|
}
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
_LOGGER.error(f"Google Gemini library not installed: {str(e)}")
|
||||||
|
raise HomeAssistantError(f"Missing dependency: {str(e)}. Please install google-genai.")
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Gemini API error: {str(e)}")
|
||||||
|
raise HomeAssistantError(f"Gemini API error: {str(e)}")
|
||||||
|
|
||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
"""Shutdown API client."""
|
"""Shutdown API client."""
|
||||||
_LOGGER.debug("Shutting down API client")
|
_LOGGER.debug("Shutting down API client")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Config flow for HA text AI integration.
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
@@ -28,13 +29,19 @@ from .const import (
|
|||||||
CONF_CONTEXT_MESSAGES,
|
CONF_CONTEXT_MESSAGES,
|
||||||
API_PROVIDER_OPENAI,
|
API_PROVIDER_OPENAI,
|
||||||
API_PROVIDER_ANTHROPIC,
|
API_PROVIDER_ANTHROPIC,
|
||||||
|
API_PROVIDER_DEEPSEEK,
|
||||||
|
API_PROVIDER_GEMINI,
|
||||||
API_PROVIDERS,
|
API_PROVIDERS,
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
|
DEFAULT_DEEPSEEK_MODEL,
|
||||||
|
DEFAULT_GEMINI_MODEL,
|
||||||
DEFAULT_TEMPERATURE,
|
DEFAULT_TEMPERATURE,
|
||||||
DEFAULT_MAX_TOKENS,
|
DEFAULT_MAX_TOKENS,
|
||||||
DEFAULT_REQUEST_INTERVAL,
|
DEFAULT_REQUEST_INTERVAL,
|
||||||
DEFAULT_OPENAI_ENDPOINT,
|
DEFAULT_OPENAI_ENDPOINT,
|
||||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||||
|
DEFAULT_DEEPSEEK_ENDPOINT,
|
||||||
|
DEFAULT_GEMINI_ENDPOINT,
|
||||||
DEFAULT_CONTEXT_MESSAGES,
|
DEFAULT_CONTEXT_MESSAGES,
|
||||||
MIN_TEMPERATURE,
|
MIN_TEMPERATURE,
|
||||||
MAX_TEMPERATURE,
|
MAX_TEMPERATURE,
|
||||||
@@ -90,9 +97,19 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._errors = {}
|
self._errors = {}
|
||||||
|
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
default_endpoint = (
|
# Selecting an endpoint by provider
|
||||||
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
|
default_endpoint = {
|
||||||
else DEFAULT_ANTHROPIC_ENDPOINT
|
API_PROVIDER_OPENAI: DEFAULT_OPENAI_ENDPOINT,
|
||||||
|
API_PROVIDER_ANTHROPIC: DEFAULT_ANTHROPIC_ENDPOINT,
|
||||||
|
API_PROVIDER_DEEPSEEK: DEFAULT_DEEPSEEK_ENDPOINT,
|
||||||
|
API_PROVIDER_GEMINI: DEFAULT_GEMINI_ENDPOINT,
|
||||||
|
}.get(self._provider, DEFAULT_OPENAI_ENDPOINT)
|
||||||
|
|
||||||
|
# Selecting the default model by provider
|
||||||
|
default_model = (
|
||||||
|
DEFAULT_DEEPSEEK_MODEL if self._provider == API_PROVIDER_DEEPSEEK else
|
||||||
|
DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else
|
||||||
|
DEFAULT_MODEL
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
@@ -100,7 +117,7 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema({
|
||||||
vol.Required(CONF_NAME, default="my_assistant"): str,
|
vol.Required(CONF_NAME, default="my_assistant"): str,
|
||||||
vol.Required(CONF_API_KEY): str,
|
vol.Required(CONF_API_KEY): str,
|
||||||
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str,
|
vol.Required(CONF_MODEL, default=default_model): str,
|
||||||
vol.Required(CONF_API_ENDPOINT, default=default_endpoint): str,
|
vol.Required(CONF_API_ENDPOINT, default=default_endpoint): str,
|
||||||
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||||
vol.Coerce(float),
|
vol.Coerce(float),
|
||||||
@@ -131,43 +148,173 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Debug log to identify what's in the input
|
||||||
|
_LOGGER.debug(f"Provider step input data: {user_input}")
|
||||||
|
|
||||||
input_copy = user_input.copy()
|
input_copy = user_input.copy()
|
||||||
|
|
||||||
|
# Check if CONF_NAME exists in input_copy and ensure it's not empty
|
||||||
|
if CONF_NAME not in input_copy or not input_copy[CONF_NAME]:
|
||||||
|
_LOGGER.warning(f"Missing name in configuration input: {input_copy}")
|
||||||
|
input_copy[CONF_NAME] = f"gemini_assistant_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||||
|
_LOGGER.info(f"Auto-generated name: {input_copy[CONF_NAME]}")
|
||||||
|
|
||||||
|
# Ensure API key is present
|
||||||
|
if CONF_API_KEY not in input_copy or not input_copy[CONF_API_KEY]:
|
||||||
|
self._errors["base"] = "invalid_auth"
|
||||||
|
_LOGGER.error("API validation error: 'api_key'")
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else DEFAULT_MODEL)): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT if self._provider == API_PROVIDER_GEMINI else DEFAULT_OPENAI_ENDPOINT)): str,
|
||||||
|
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_MAX_TOKENS, default=input_copy.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=input_copy.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
default=input_copy.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
default=input_copy.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
errors=self._errors
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Validate and normalize the name
|
||||||
normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME])
|
normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME])
|
||||||
input_copy[CONF_NAME] = normalized_name
|
input_copy[CONF_NAME] = normalized_name
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="provider",
|
step_id="provider",
|
||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema({
|
||||||
vol.Required(CONF_NAME, default=input_copy[CONF_NAME]): str,
|
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||||
vol.Required(CONF_API_KEY, default=input_copy[CONF_API_KEY]): str,
|
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
|
||||||
vol.Required(CONF_MODEL, default=input_copy[CONF_MODEL]): str,
|
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else DEFAULT_MODEL)): str,
|
||||||
vol.Required(CONF_API_ENDPOINT, default=input_copy[CONF_API_ENDPOINT]): str,
|
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT if self._provider == API_PROVIDER_GEMINI else DEFAULT_OPENAI_ENDPOINT)): str,
|
||||||
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
|
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
|
||||||
vol.Coerce(float),
|
vol.Coerce(float),
|
||||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_MAX_TOKENS, default=input_copy.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=input_copy.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
default=input_copy.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
default=input_copy.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100)
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
errors={"name": str(e)}
|
errors={"name": str(e)}
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not await self._async_validate_api(input_copy):
|
# Special handling for Gemini API validation
|
||||||
return self.async_show_form(
|
if self._provider == API_PROVIDER_GEMINI:
|
||||||
step_id="provider",
|
# For Gemini, we just check if API key is present as there's no simple endpoint to validate
|
||||||
data_schema=vol.Schema({
|
if not input_copy.get(CONF_API_KEY):
|
||||||
}),
|
self._errors["base"] = "invalid_auth"
|
||||||
errors=self._errors
|
_LOGGER.error("API validation error: 'api_key'")
|
||||||
)
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL)): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT)): str,
|
||||||
|
# Other fields remain the same
|
||||||
|
}),
|
||||||
|
errors=self._errors
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# For other providers, validate API connection
|
||||||
|
if not await self._async_validate_api(input_copy):
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||||
|
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
|
||||||
|
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_MODEL)): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_OPENAI_ENDPOINT)): str,
|
||||||
|
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_MAX_TOKENS, default=input_copy.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=input_copy.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
default=input_copy.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
default=input_copy.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
errors=self._errors
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Handle any unexpected exceptions during validation
|
||||||
|
_LOGGER.exception("Unexpected error during API validation")
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="provider",
|
step_id="provider",
|
||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
|
||||||
|
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
|
||||||
|
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else DEFAULT_MODEL)): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT if self._provider == API_PROVIDER_GEMINI else DEFAULT_OPENAI_ENDPOINT)): str,
|
||||||
|
# Other fields remain the same
|
||||||
}),
|
}),
|
||||||
errors={"base": str(e)}
|
errors={"base": str(e)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# All validation passed, create the entry
|
||||||
return await self._create_entry(input_copy)
|
return await self._create_entry(input_copy)
|
||||||
|
|
||||||
def _validate_and_normalize_name(self, name: str) -> str:
|
def _validate_and_normalize_name(self, name: str) -> str:
|
||||||
@@ -205,23 +352,34 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
|
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
|
||||||
"""Validate API connection."""
|
"""Validate API connection."""
|
||||||
try:
|
try:
|
||||||
|
if CONF_API_KEY not in user_input:
|
||||||
|
_LOGGER.error("API validation error: 'api_key'")
|
||||||
|
self._errors["base"] = "invalid_auth"
|
||||||
|
return False
|
||||||
|
|
||||||
session = async_get_clientsession(self.hass)
|
session = async_get_clientsession(self.hass)
|
||||||
headers = self._get_api_headers(user_input)
|
headers = self._get_api_headers(user_input)
|
||||||
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
|
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
|
||||||
|
|
||||||
check_url = (
|
if self._provider == API_PROVIDER_GEMINI:
|
||||||
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
|
if not user_input[CONF_API_KEY]:
|
||||||
else f"{endpoint}/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
async with session.get(check_url, headers=headers) as response:
|
|
||||||
if response.status == 401:
|
|
||||||
self._errors["base"] = "invalid_auth"
|
self._errors["base"] = "invalid_auth"
|
||||||
return False
|
return False
|
||||||
elif response.status not in [200, 404]:
|
|
||||||
self._errors["base"] = "cannot_connect"
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
else:
|
||||||
|
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:
|
except Exception as err:
|
||||||
_LOGGER.error("API validation error: %s", str(err))
|
_LOGGER.error("API validation error: %s", str(err))
|
||||||
@@ -230,6 +388,9 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
|
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
|
||||||
"""Get API headers based on provider."""
|
"""Get API headers based on provider."""
|
||||||
|
if CONF_API_KEY not in user_input:
|
||||||
|
return {"Content-Type": "application/json"}
|
||||||
|
|
||||||
api_key = user_input[CONF_API_KEY]
|
api_key = user_input[CONF_API_KEY]
|
||||||
|
|
||||||
if self._provider == API_PROVIDER_ANTHROPIC:
|
if self._provider == API_PROVIDER_ANTHROPIC:
|
||||||
@@ -238,6 +399,11 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"anthropic-version": "2023-06-01",
|
"anthropic-version": "2023-06-01",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
|
elif self._provider == API_PROVIDER_GEMINI:
|
||||||
|
return {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
"Authorization": f"Bearer {api_key}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
@@ -250,6 +416,12 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
unique_id = f"{DOMAIN}_{normalized_name}_{self._provider}".lower()
|
unique_id = f"{DOMAIN}_{normalized_name}_{self._provider}".lower()
|
||||||
|
|
||||||
|
default_model = (
|
||||||
|
DEFAULT_DEEPSEEK_MODEL if self._provider == API_PROVIDER_DEEPSEEK else
|
||||||
|
DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else
|
||||||
|
DEFAULT_MODEL
|
||||||
|
)
|
||||||
|
|
||||||
entry_data = {
|
entry_data = {
|
||||||
CONF_API_PROVIDER: self._provider,
|
CONF_API_PROVIDER: self._provider,
|
||||||
CONF_NAME: instance_name,
|
CONF_NAME: instance_name,
|
||||||
@@ -257,7 +429,7 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_API_KEY: user_input.get(CONF_API_KEY),
|
CONF_API_KEY: user_input.get(CONF_API_KEY),
|
||||||
CONF_API_ENDPOINT: user_input.get(CONF_API_ENDPOINT),
|
CONF_API_ENDPOINT: user_input.get(CONF_API_ENDPOINT),
|
||||||
"unique_id": unique_id,
|
"unique_id": unique_id,
|
||||||
CONF_MODEL: user_input.get(CONF_MODEL, DEFAULT_MODEL),
|
CONF_MODEL: user_input.get(CONF_MODEL, default_model),
|
||||||
CONF_TEMPERATURE: user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
CONF_TEMPERATURE: user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
||||||
CONF_MAX_TOKENS: user_input.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
CONF_MAX_TOKENS: user_input.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
||||||
CONF_REQUEST_INTERVAL: user_input.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
CONF_REQUEST_INTERVAL: user_input.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
||||||
@@ -296,13 +468,20 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
return self.async_create_entry(title="", data=user_input)
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
current_data = {**self.config_entry.data, **self.config_entry.options}
|
current_data = {**self.config_entry.data, **self.config_entry.options}
|
||||||
|
provider = current_data.get(CONF_API_PROVIDER)
|
||||||
|
|
||||||
|
default_model = (
|
||||||
|
DEFAULT_DEEPSEEK_MODEL if provider == API_PROVIDER_DEEPSEEK else
|
||||||
|
DEFAULT_GEMINI_MODEL if provider == API_PROVIDER_GEMINI else
|
||||||
|
DEFAULT_MODEL
|
||||||
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="init",
|
step_id="init",
|
||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema({
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_MODEL,
|
CONF_MODEL,
|
||||||
default=current_data.get(CONF_MODEL, DEFAULT_MODEL)
|
default=current_data.get(CONF_MODEL, default_model)
|
||||||
): str,
|
): str,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_TEMPERATURE,
|
CONF_TEMPERATURE,
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ Constants for the HA text AI integration.
|
|||||||
@github: https://github.com/smkrv/ha-text-ai
|
@github: https://github.com/smkrv/ha-text-ai
|
||||||
@source: https://github.com/smkrv/ha-text-ai
|
@source: https://github.com/smkrv/ha-text-ai
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
from typing import Final
|
from typing import Final
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
|
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
import logging
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Domain and platforms
|
# Domain and platforms
|
||||||
DOMAIN: Final = "ha_text_ai"
|
DOMAIN: Final = "ha_text_ai"
|
||||||
@@ -19,15 +23,37 @@ PLATFORMS: list[str] = ["sensor"]
|
|||||||
CONF_API_PROVIDER: Final = "api_provider"
|
CONF_API_PROVIDER: Final = "api_provider"
|
||||||
API_PROVIDER_OPENAI: Final = "openai"
|
API_PROVIDER_OPENAI: Final = "openai"
|
||||||
API_PROVIDER_ANTHROPIC: Final = "anthropic"
|
API_PROVIDER_ANTHROPIC: Final = "anthropic"
|
||||||
|
API_PROVIDER_DEEPSEEK: Final = "deepseek"
|
||||||
|
API_PROVIDER_GEMINI: Final = "gemini"
|
||||||
|
|
||||||
API_PROVIDERS: Final = [
|
API_PROVIDERS: Final = [
|
||||||
API_PROVIDER_OPENAI,
|
API_PROVIDER_OPENAI,
|
||||||
API_PROVIDER_ANTHROPIC
|
API_PROVIDER_ANTHROPIC,
|
||||||
|
API_PROVIDER_DEEPSEEK,
|
||||||
|
API_PROVIDER_GEMINI
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Read version from manifest.json
|
||||||
|
MANIFEST_PATH = os.path.join(os.path.dirname(__file__), "manifest.json")
|
||||||
|
try:
|
||||||
|
with open(MANIFEST_PATH) as manifest_file:
|
||||||
|
manifest = json.load(manifest_file)
|
||||||
|
VERSION = manifest.get("version", "unknown")
|
||||||
|
except FileNotFoundError:
|
||||||
|
VERSION = "unknown"
|
||||||
|
_LOGGER.warning("manifest.json not found at %s", MANIFEST_PATH)
|
||||||
|
except json.JSONDecodeError as err:
|
||||||
|
VERSION = "unknown"
|
||||||
|
_LOGGER.error("Error decoding JSON from manifest.json: %s", err)
|
||||||
|
except Exception as err:
|
||||||
|
VERSION = "unknown"
|
||||||
|
_LOGGER.error("Error reading manifest.json: %s", err)
|
||||||
|
|
||||||
# Default endpoints
|
# Default endpoints
|
||||||
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
|
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
|
||||||
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
|
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
|
||||||
|
DEFAULT_DEEPSEEK_ENDPOINT: Final = "https://api.deepseek.com"
|
||||||
|
DEFAULT_GEMINI_ENDPOINT: Final = "https://generativelanguage.googleapis.com/v1beta"
|
||||||
|
|
||||||
# Configuration constants
|
# Configuration constants
|
||||||
CONF_MODEL: Final = "model"
|
CONF_MODEL: Final = "model"
|
||||||
@@ -41,10 +67,14 @@ CONF_IS_ANTHROPIC: Final = "is_anthropic"
|
|||||||
CONF_CONTEXT_MESSAGES: Final = "context_messages"
|
CONF_CONTEXT_MESSAGES: Final = "context_messages"
|
||||||
|
|
||||||
ABSOLUTE_MAX_HISTORY_SIZE = 500
|
ABSOLUTE_MAX_HISTORY_SIZE = 500
|
||||||
MAX_ENTRY_SIZE = 1 * 1024 * 1024
|
MAX_ATTRIBUTE_SIZE = 4 * 1024
|
||||||
|
MAX_HISTORY_FILE_SIZE = 1 * 1024 * 1024
|
||||||
|
ICONS_SUBDOMAIN = "icons"
|
||||||
|
|
||||||
# Default values
|
# Default values
|
||||||
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
||||||
|
DEFAULT_DEEPSEEK_MODEL: Final = "deepseek-chat"
|
||||||
|
DEFAULT_GEMINI_MODEL: Final = "gemini-2.0-flash"
|
||||||
DEFAULT_TEMPERATURE: Final = 0.1
|
DEFAULT_TEMPERATURE: Final = 0.1
|
||||||
DEFAULT_MAX_TOKENS: Final = 1000
|
DEFAULT_MAX_TOKENS: Final = 1000
|
||||||
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
||||||
@@ -54,11 +84,13 @@ DEFAULT_NAME: Final = "HA Text AI"
|
|||||||
DEFAULT_NAME_PREFIX = "ha_text_ai"
|
DEFAULT_NAME_PREFIX = "ha_text_ai"
|
||||||
DEFAULT_CONTEXT_MESSAGES: Final = 5
|
DEFAULT_CONTEXT_MESSAGES: Final = 5
|
||||||
|
|
||||||
|
TRUNCATION_INDICATOR = " ... "
|
||||||
|
|
||||||
# Parameter constraints
|
# Parameter constraints
|
||||||
MIN_TEMPERATURE: Final = 0.0
|
MIN_TEMPERATURE: Final = 0.0
|
||||||
MAX_TEMPERATURE: Final = 2.0
|
MAX_TEMPERATURE: Final = 2.0
|
||||||
MIN_MAX_TOKENS: Final = 1
|
MIN_MAX_TOKENS: Final = 1
|
||||||
MAX_MAX_TOKENS: Final = 4096
|
MAX_MAX_TOKENS: Final = 100000
|
||||||
MIN_REQUEST_INTERVAL: Final = 0.1
|
MIN_REQUEST_INTERVAL: Final = 0.1
|
||||||
MAX_REQUEST_INTERVAL: Final = 60.0
|
MAX_REQUEST_INTERVAL: Final = 60.0
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 259 KiB |
@@ -16,6 +16,7 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"openai>=1.12.0",
|
"openai>=1.12.0",
|
||||||
"anthropic>=0.8.0",
|
"anthropic>=0.8.0",
|
||||||
|
"google-genai>=1.16.0",
|
||||||
"aiohttp>=3.8.0",
|
"aiohttp>=3.8.0",
|
||||||
"async-timeout>=4.0.0",
|
"async-timeout>=4.0.0",
|
||||||
"certifi>=2024.2.2"
|
"certifi>=2024.2.2"
|
||||||
@@ -23,6 +24,6 @@
|
|||||||
"single_config_entry": false,
|
"single_config_entry": false,
|
||||||
"ssdp": [],
|
"ssdp": [],
|
||||||
"usb": [],
|
"usb": [],
|
||||||
"version": "2.0.5-beta",
|
"version": "2.1.9",
|
||||||
"zeroconf": []
|
"zeroconf": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Sensor platform for HA Text AI.
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
@@ -67,6 +68,8 @@ from .const import (
|
|||||||
ENTITY_ICON_PROCESSING,
|
ENTITY_ICON_PROCESSING,
|
||||||
DEFAULT_NAME_PREFIX,
|
DEFAULT_NAME_PREFIX,
|
||||||
CONF_MAX_HISTORY_SIZE,
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
MAX_ATTRIBUTE_SIZE,
|
||||||
|
VERSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .coordinator import HATextAICoordinator
|
from .coordinator import HATextAICoordinator
|
||||||
@@ -153,7 +156,7 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
name=self._attr_name,
|
name=self._attr_name,
|
||||||
manufacturer="Community",
|
manufacturer="Community",
|
||||||
model=f"{model} ({api_provider} provider)",
|
model=f"{model} ({api_provider} provider)",
|
||||||
sw_version="1.0.0",
|
sw_version=VERSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@@ -178,12 +181,29 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
|
|
||||||
def _sanitize_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
|
def _sanitize_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Sanitize all attributes for JSON serialization."""
|
"""Sanitize all attributes for JSON serialization."""
|
||||||
return {
|
sanitized = {
|
||||||
key: self._sanitize_value(value)
|
key: self._sanitize_value(value)
|
||||||
for key, value in attributes.items()
|
for key, value in attributes.items()
|
||||||
if value is not None
|
if value is not None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Log metrics for debugging
|
||||||
|
metrics_keys = [
|
||||||
|
METRIC_TOTAL_TOKENS,
|
||||||
|
METRIC_PROMPT_TOKENS,
|
||||||
|
METRIC_COMPLETION_TOKENS,
|
||||||
|
METRIC_SUCCESSFUL_REQUESTS,
|
||||||
|
METRIC_FAILED_REQUESTS,
|
||||||
|
METRIC_AVERAGE_LATENCY,
|
||||||
|
METRIC_MAX_LATENCY,
|
||||||
|
METRIC_MIN_LATENCY,
|
||||||
|
]
|
||||||
|
|
||||||
|
metrics_values = {k: sanitized.get(k) for k in metrics_keys if k in sanitized}
|
||||||
|
_LOGGER.debug(f"Metrics for {self.entity_id}: {metrics_values}")
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the native value of the sensor."""
|
"""Return the native value of the sensor."""
|
||||||
@@ -212,68 +232,65 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
data = self.coordinator.data
|
data = self.coordinator.data
|
||||||
|
metrics = data.get("metrics", {})
|
||||||
|
|
||||||
|
# Base attributes
|
||||||
attributes = {
|
attributes = {
|
||||||
ATTR_MODEL: self._config_entry.data.get(CONF_MODEL, "Unknown"),
|
ATTR_MODEL: self._config_entry.data.get(CONF_MODEL, "Unknown"),
|
||||||
ATTR_API_PROVIDER: self._config_entry.data.get(
|
ATTR_API_PROVIDER: self._config_entry.data.get(CONF_API_PROVIDER, "Unknown"),
|
||||||
CONF_API_PROVIDER, "Unknown"
|
|
||||||
),
|
|
||||||
ATTR_API_STATUS: self._current_state,
|
ATTR_API_STATUS: self._current_state,
|
||||||
ATTR_TOTAL_ERRORS: self._error_count,
|
ATTR_TOTAL_ERRORS: metrics.get("total_errors", 0),
|
||||||
ATTR_LAST_ERROR: self._last_error,
|
|
||||||
"instance_name": self._instance_name,
|
"instance_name": self._instance_name,
|
||||||
"normalized_name": self._normalized_name,
|
"normalized_name": self._normalized_name,
|
||||||
ATTR_SYSTEM_PROMPT: data.get("system_prompt"),
|
ATTR_SYSTEM_PROMPT: (data.get("system_prompt", "")[:MAX_ATTRIBUTE_SIZE]
|
||||||
|
if data.get("system_prompt") else None),
|
||||||
ATTR_IS_PROCESSING: data.get("is_processing", False),
|
ATTR_IS_PROCESSING: data.get("is_processing", False),
|
||||||
ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False),
|
ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False),
|
||||||
ATTR_IS_MAINTENANCE: data.get("is_maintenance", False),
|
ATTR_IS_MAINTENANCE: data.get("is_maintenance", False),
|
||||||
ATTR_ENDPOINT_STATUS: data.get("endpoint_status", "unknown"),
|
ATTR_ENDPOINT_STATUS: data.get("endpoint_status", "unknown"),
|
||||||
ATTR_UPTIME: data.get("uptime", 0),
|
ATTR_UPTIME: round(data.get("uptime", 0), 2),
|
||||||
ATTR_HISTORY_SIZE: data.get("history_size", 0),
|
ATTR_HISTORY_SIZE: data.get("history_size", 0),
|
||||||
ATTR_CONVERSATION_HISTORY: data.get("conversation_history", []),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add metrics
|
# History limit
|
||||||
metrics = data.get("metrics", {})
|
conversation_history = data.get("conversation_history", [])
|
||||||
if isinstance(metrics, dict):
|
if conversation_history:
|
||||||
self._metrics = metrics
|
limited_history = []
|
||||||
attributes.update(
|
for entry in conversation_history:
|
||||||
{
|
limited_entry = {
|
||||||
METRIC_TOTAL_TOKENS: metrics.get("total_tokens", 0),
|
"timestamp": entry["timestamp"],
|
||||||
METRIC_PROMPT_TOKENS: metrics.get("prompt_tokens", 0),
|
"question": entry["question"][:MAX_ATTRIBUTE_SIZE],
|
||||||
METRIC_COMPLETION_TOKENS: metrics.get("completion_tokens", 0),
|
"response": entry["response"][:MAX_ATTRIBUTE_SIZE]
|
||||||
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")),
|
|
||||||
}
|
}
|
||||||
)
|
limited_history.append(limited_entry)
|
||||||
|
attributes[ATTR_CONVERSATION_HISTORY] = limited_history
|
||||||
|
|
||||||
# Add last response
|
# Metrics
|
||||||
|
if isinstance(metrics, dict):
|
||||||
|
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: round(metrics.get("average_latency", 0), 2),
|
||||||
|
METRIC_MAX_LATENCY: round(metrics.get("max_latency", 0), 2),
|
||||||
|
METRIC_MIN_LATENCY: (metrics.get("min_latency")
|
||||||
|
if metrics.get("min_latency") != float("inf")
|
||||||
|
else None),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Last response handling
|
||||||
last_response = data.get("last_response", {})
|
last_response = data.get("last_response", {})
|
||||||
if isinstance(last_response, dict):
|
if isinstance(last_response, dict):
|
||||||
self._last_response = last_response
|
attributes.update({
|
||||||
attributes.update(
|
ATTR_RESPONSE: last_response.get("response", "")[:MAX_ATTRIBUTE_SIZE],
|
||||||
{
|
ATTR_QUESTION: last_response.get("question", "")[:MAX_ATTRIBUTE_SIZE],
|
||||||
ATTR_RESPONSE: last_response.get("response", ""),
|
"last_model": last_response.get("model", ""),
|
||||||
ATTR_QUESTION: last_response.get("question", ""),
|
"last_timestamp": last_response.get("timestamp", ""),
|
||||||
"last_model": last_response.get("model", ""),
|
"last_error": (last_response.get("error", "")[:MAX_ATTRIBUTE_SIZE]
|
||||||
"last_timestamp": last_response.get("timestamp", ""),
|
if last_response.get("error") else None),
|
||||||
"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)
|
return self._sanitize_attributes(attributes)
|
||||||
|
|
||||||
@@ -299,6 +316,12 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
|
|
||||||
self._is_processing = data.get("is_processing", False)
|
self._is_processing = data.get("is_processing", False)
|
||||||
|
|
||||||
|
# Update metrics
|
||||||
|
metrics = data.get("metrics", {})
|
||||||
|
if isinstance(metrics, dict):
|
||||||
|
self._metrics.update(metrics)
|
||||||
|
_LOGGER.debug(f"Updated metrics for {self.entity_id}: {self._metrics}")
|
||||||
|
|
||||||
# Update conversation history and system prompt
|
# Update conversation history and system prompt
|
||||||
self._conversation_history = data.get("conversation_history", [])
|
self._conversation_history = data.get("conversation_history", [])
|
||||||
self._system_prompt = data.get("system_prompt")
|
self._system_prompt = data.get("system_prompt")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ ask_question:
|
|||||||
description: >-
|
description: >-
|
||||||
Send a question to the AI model and receive a detailed response.
|
Send a question to the AI model and receive a detailed response.
|
||||||
The response will be stored in the conversation history and can be retrieved later.
|
The response will be stored in the conversation history and can be retrieved later.
|
||||||
|
This service now returns response data directly, eliminating the need to read from sensors.
|
||||||
fields:
|
fields:
|
||||||
instance:
|
instance:
|
||||||
name: Instance
|
name: Instance
|
||||||
@@ -63,13 +64,13 @@ ask_question:
|
|||||||
|
|
||||||
max_tokens:
|
max_tokens:
|
||||||
name: Max Tokens
|
name: Max Tokens
|
||||||
description: Maximum length of the response (1-4096 tokens)
|
description: Maximum length of the response (tokens)
|
||||||
required: false
|
required: false
|
||||||
default: 1000
|
default: 1000
|
||||||
selector:
|
selector:
|
||||||
number:
|
number:
|
||||||
min: 1
|
min: 1
|
||||||
max: 4096
|
max: 100000
|
||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"model": "Zu verwendendes AI-Modell",
|
"model": "Zu verwendendes AI-Modell",
|
||||||
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
||||||
"temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)",
|
"temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)",
|
||||||
"max_tokens": "Maximale Länge der Antwort (1-4096 Token)",
|
"max_tokens": "Maximale Länge der Antwort (1-100000 Token)",
|
||||||
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
||||||
"context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)",
|
"context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)",
|
||||||
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
|
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"api_key": "API-Schlüssel zur Authentifizierung",
|
"api_key": "API-Schlüssel zur Authentifizierung",
|
||||||
"model": "Zu verwendendes AI-Modell",
|
"model": "Zu verwendendes AI-Modell",
|
||||||
"temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)",
|
"temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)",
|
||||||
"max_tokens": "Maximale Länge der Antwort (1-4096 Token)",
|
"max_tokens": "Maximale Länge der Antwort (1-100000 Token)",
|
||||||
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
||||||
"api_provider": "API-Anbieter",
|
"api_provider": "API-Anbieter",
|
||||||
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "AI-Modell",
|
"model": "AI-Modell",
|
||||||
"temperature": "Kreativität der Antwort (0-2)",
|
"temperature": "Kreativität der Antwort (0-2)",
|
||||||
"max_tokens": "Maximale Länge der Antwort (1-4096)",
|
"max_tokens": "Maximale Länge der Antwort (1-100000)",
|
||||||
"request_interval": "Minimale Anfrageintervall (0,1-60 Sekunden)",
|
"request_interval": "Minimale Anfrageintervall (0,1-60 Sekunden)",
|
||||||
"context_messages": "Anzahl der vorherigen Nachrichten, die im Kontext enthalten sein sollen (1-20)",
|
"context_messages": "Anzahl der vorherigen Nachrichten, die im Kontext enthalten sein sollen (1-20)",
|
||||||
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
|
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
|
||||||
@@ -88,15 +88,17 @@
|
|||||||
"selector": {
|
"selector": {
|
||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (kompatibel)",
|
"openai": "OpenAI (compatible)",
|
||||||
"anthropic": "Anthropic (kompatibel)"
|
"anthropic": "Anthropic (compatible)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Frage stellen (HA Text AI)",
|
"name": "Frage stellen (HA Text AI)",
|
||||||
"description": "Stellen Sie eine Frage an das AI-Modell und erhalten Sie eine detaillierte Antwort. Die Antwort wird im Gesprächsverlauf gespeichert und kann später abgerufen werden.",
|
"description": "Stellen Sie eine Frage an das AI-Modell und erhalten Sie eine detaillierte Antwort. Dieser Service gibt jetzt Antwortdaten direkt zurück, wodurch separate Textsensoren und die 255-Zeichen-Begrenzung überflüssig werden. Die Antwort wird auch im Gesprächsverlauf gespeichert.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instanz",
|
"name": "Instanz",
|
||||||
@@ -124,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Max Tokens",
|
"name": "Max Tokens",
|
||||||
"description": "Maximale Länge der Antwort (1-4096 Token)"
|
"description": "Maximale Länge der Antwort (1-100000 Token)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"model": "AI model to use",
|
"model": "AI model to use",
|
||||||
"api_endpoint": "Custom API endpoint URL (optional)",
|
"api_endpoint": "Custom API endpoint URL (optional)",
|
||||||
"temperature": "Response creativity (0-2, lower = more focused)",
|
"temperature": "Response creativity (0-2, lower = more focused)",
|
||||||
"max_tokens": "Maximum response length (1-4096 tokens)",
|
"max_tokens": "Maximum response length (1-100000 tokens)",
|
||||||
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
||||||
"context_messages": "Number of context messages to retain (1-20)",
|
"context_messages": "Number of context messages to retain (1-20)",
|
||||||
"max_history_size": "Maximum conversation history size (1-100)"
|
"max_history_size": "Maximum conversation history size (1-100)"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"api_key": "API key for authentication",
|
"api_key": "API key for authentication",
|
||||||
"model": "AI model to use",
|
"model": "AI model to use",
|
||||||
"temperature": "Response creativity (0-2, lower = more focused)",
|
"temperature": "Response creativity (0-2, lower = more focused)",
|
||||||
"max_tokens": "Maximum response length (1-4096 tokens)",
|
"max_tokens": "Maximum response length (1-100000 tokens)",
|
||||||
"api_endpoint": "Custom API endpoint URL (optional)",
|
"api_endpoint": "Custom API endpoint URL (optional)",
|
||||||
"api_provider": "API Provider",
|
"api_provider": "API Provider",
|
||||||
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "AI model",
|
"model": "AI model",
|
||||||
"temperature": "Response creativity (0-2)",
|
"temperature": "Response creativity (0-2)",
|
||||||
"max_tokens": "Maximum response length (1-4096)",
|
"max_tokens": "Maximum response length (1-100000)",
|
||||||
"request_interval": "Minimum request interval (0.1-60 seconds)",
|
"request_interval": "Minimum request interval (0.1-60 seconds)",
|
||||||
"context_messages": "Number of previous messages to include in context (1-20)",
|
"context_messages": "Number of previous messages to include in context (1-20)",
|
||||||
"max_history_size": "Maximum conversation history size (1-100)"
|
"max_history_size": "Maximum conversation history size (1-100)"
|
||||||
@@ -86,17 +86,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selector": {
|
"selector": {
|
||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (compatible)",
|
"openai": "OpenAI (compatible)",
|
||||||
"anthropic": "Anthropic (compatible)"
|
"anthropic": "Anthropic (compatible)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Ask Question (HA Text AI)",
|
"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.",
|
"description": "Send a question to the AI model and receive a detailed response. This service now returns response data directly, eliminating the need for separate text sensors and the 255-character limitation. The response will also be stored in the conversation history.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instance",
|
"name": "Instance",
|
||||||
@@ -124,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Max Tokens",
|
"name": "Max Tokens",
|
||||||
"description": "Maximum length of the response (1-4096 tokens)"
|
"description": "Maximum length of the response (1-100000 tokens)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"model": "Modelo de IA a utilizar",
|
"model": "Modelo de IA a utilizar",
|
||||||
"api_endpoint": "URL del endpoint de API personalizado (opcional)",
|
"api_endpoint": "URL del endpoint de API personalizado (opcional)",
|
||||||
"temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)",
|
"temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)",
|
||||||
"max_tokens": "Longitud máxima de la respuesta (1-4096 tokens)",
|
"max_tokens": "Longitud máxima de la respuesta (1-100000 tokens)",
|
||||||
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)",
|
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)",
|
||||||
"context_messages": "Número de mensajes de contexto a retener (1-20)",
|
"context_messages": "Número de mensajes de contexto a retener (1-20)",
|
||||||
"max_history_size": "Tamaño máximo del historial de conversación (1-100)"
|
"max_history_size": "Tamaño máximo del historial de conversación (1-100)"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"api_key": "Clave API para autenticación",
|
"api_key": "Clave API para autenticación",
|
||||||
"model": "Modelo de IA a utilizar",
|
"model": "Modelo de IA a utilizar",
|
||||||
"temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)",
|
"temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)",
|
||||||
"max_tokens": "Longitud máxima de la respuesta (1-4096 tokens)",
|
"max_tokens": "Longitud máxima de la respuesta (1-100000 tokens)",
|
||||||
"api_endpoint": "URL del endpoint de API personalizado (opcional)",
|
"api_endpoint": "URL del endpoint de API personalizado (opcional)",
|
||||||
"api_provider": "Proveedor de API",
|
"api_provider": "Proveedor de API",
|
||||||
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)",
|
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "Modelo de IA",
|
"model": "Modelo de IA",
|
||||||
"temperature": "Creatividad de la respuesta (0-2)",
|
"temperature": "Creatividad de la respuesta (0-2)",
|
||||||
"max_tokens": "Longitud máxima de la respuesta (1-4096)",
|
"max_tokens": "Longitud máxima de la respuesta (1-100000)",
|
||||||
"request_interval": "Intervalo mínimo de solicitud (0.1-60 segundos)",
|
"request_interval": "Intervalo mínimo de solicitud (0.1-60 segundos)",
|
||||||
"context_messages": "Número de mensajes anteriores a incluir en el contexto (1-20)",
|
"context_messages": "Número de mensajes anteriores a incluir en el contexto (1-20)",
|
||||||
"max_history_size": "Tamaño máximo del historial de conversación (1-100)"
|
"max_history_size": "Tamaño máximo del historial de conversación (1-100)"
|
||||||
@@ -89,14 +89,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (compatible)",
|
"openai": "OpenAI (compatible)",
|
||||||
"anthropic": "Anthropic (compatible)"
|
"anthropic": "Anthropic (compatible)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Hacer Pregunta (IA de Texto de HA)",
|
"name": "Hacer Pregunta (HA Text AI)",
|
||||||
"description": "Envía una pregunta al modelo de IA y recibe una respuesta detallada. La respuesta se almacenará en el historial de conversación y se podrá recuperar más tarde.",
|
"description": "Envía una pregunta al modelo de IA y recibe una respuesta detallada. Este servicio ahora devuelve datos de respuesta directamente, eliminando la necesidad de sensores de texto separados y la limitación de 255 caracteres. La respuesta también se almacenará en el historial de conversación.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instancia",
|
"name": "Instancia",
|
||||||
@@ -124,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Máx. Tokens",
|
"name": "Máx. Tokens",
|
||||||
"description": "Longitud máxima de la respuesta (1-4096 tokens)"
|
"description": "Longitud máxima de la respuesta (1-100000 tokens)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
|
||||||
"title": "एआई प्रदाता चुनें",
|
|
||||||
"description": "इस उदाहरण के लिए किस एआई सेवा प्रदाता का उपयोग करना है, चुनें।",
|
|
||||||
"data": {
|
|
||||||
"api_provider": "एपीआई प्रदाता",
|
|
||||||
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
|
|
||||||
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "प्रदाता सेटिंग्स",
|
"title": "प्रदाता सेटिंग्स",
|
||||||
"description": "आपके द्वारा चुने गए एआई प्रदाता के लिए कनेक्शन विवरण प्रदान करें।",
|
"description": "आपके द्वारा चुने गए एआई प्रदाता के लिए कनेक्शन विवरण प्रदान करें।",
|
||||||
@@ -19,7 +10,7 @@
|
|||||||
"model": "उपयोग करने के लिए एआई मॉडल",
|
"model": "उपयोग करने के लिए एआई मॉडल",
|
||||||
"api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)",
|
"api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)",
|
||||||
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)",
|
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)",
|
||||||
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-4096 टोकन)",
|
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)",
|
||||||
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
|
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
|
||||||
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
|
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
|
||||||
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
||||||
@@ -33,7 +24,7 @@
|
|||||||
"api_key": "प्रमाणीकरण के लिए एपीआई कुंजी",
|
"api_key": "प्रमाणीकरण के लिए एपीआई कुंजी",
|
||||||
"model": "उपयोग करने के लिए एआई मॉडल",
|
"model": "उपयोग करने के लिए एआई मॉडल",
|
||||||
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)",
|
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)",
|
||||||
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-4096 टोकन)",
|
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)",
|
||||||
"api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)",
|
"api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)",
|
||||||
"api_provider": "एपीआई प्रदाता",
|
"api_provider": "एपीआई प्रदाता",
|
||||||
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
|
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
|
||||||
@@ -77,7 +68,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "एआई मॉडल",
|
"model": "एआई मॉडल",
|
||||||
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2)",
|
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2)",
|
||||||
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-4096)",
|
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-100000)",
|
||||||
"request_interval": "न्यूनतम अनुरोध अंतराल (0.1-60 सेकंड)",
|
"request_interval": "न्यूनतम अनुरोध अंतराल (0.1-60 सेकंड)",
|
||||||
"context_messages": "संदर्भ में शामिल करने के लिए पिछले संदेशों की संख्या (1-20)",
|
"context_messages": "संदर्भ में शामिल करने के लिए पिछले संदेशों की संख्या (1-20)",
|
||||||
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
||||||
@@ -89,14 +80,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (अनुकूलित)",
|
"openai": "OpenAI (अनुकूलित)",
|
||||||
"anthropic": "Anthropic (अनुकूलित)"
|
"anthropic": "Anthropic (अनुकूलित)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "प्रश्न पूछें (एचए टेक्स्ट एआई)",
|
"name": "प्रश्न पूछें (HA Text AI)",
|
||||||
"description": "एआई मॉडल को एक प्रश्न भेजें और विस्तृत प्रतिक्रिया प्राप्त करें। प्रतिक्रिया बातचीत के इतिहास में संग्रहीत की जाएगी और बाद में पुनर्प्राप्त की जा सकती है।",
|
"description": "AI मॉडल को प्रश्न भेजें और विस्तृत उत्तर प्राप्त करें। यह सेवा अब प्रत्यक्ष रूप से प्रतिक्रिया डेटा वापस करती है, अलग टेक्स्ट सेंसर की आवश्यकता और 255 वर्ण की सीमा को समाप्त करती है। प्रतिक्रिया को बातचीत के इतिहास में भी संग्रहीत किया जाएगा।",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "उदाहरण",
|
"name": "उदाहरण",
|
||||||
@@ -124,7 +117,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "अधिकतम टोकन",
|
"name": "अधिकतम टोकन",
|
||||||
"description": "प्रतिक्रिया की अधिकतम लंबाई (1-4096 टोकन)"
|
"description": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"model": "Modello AI da utilizzare",
|
"model": "Modello AI da utilizzare",
|
||||||
"api_endpoint": "URL dell'endpoint API personalizzato (opzionale)",
|
"api_endpoint": "URL dell'endpoint API personalizzato (opzionale)",
|
||||||
"temperature": "Creatività della risposta (0-2, più basso = più focalizzato)",
|
"temperature": "Creatività della risposta (0-2, più basso = più focalizzato)",
|
||||||
"max_tokens": "Lunghezza massima della risposta (1-4096 token)",
|
"max_tokens": "Lunghezza massima della risposta (1-100000 token)",
|
||||||
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)",
|
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)",
|
||||||
"context_messages": "Numero di messaggi di contesto da mantenere (1-20)",
|
"context_messages": "Numero di messaggi di contesto da mantenere (1-20)",
|
||||||
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
|
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"api_key": "Chiave API per l'autenticazione",
|
"api_key": "Chiave API per l'autenticazione",
|
||||||
"model": "Modello AI da utilizzare",
|
"model": "Modello AI da utilizzare",
|
||||||
"temperature": "Creatività della risposta (0-2, più basso = più focalizzato)",
|
"temperature": "Creatività della risposta (0-2, più basso = più focalizzato)",
|
||||||
"max_tokens": "Lunghezza massima della risposta (1-4096 token)",
|
"max_tokens": "Lunghezza massima della risposta (1-100000 token)",
|
||||||
"api_endpoint": "URL dell'endpoint API personalizzato (opzionale)",
|
"api_endpoint": "URL dell'endpoint API personalizzato (opzionale)",
|
||||||
"api_provider": "Fornitore API",
|
"api_provider": "Fornitore API",
|
||||||
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)",
|
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "Modello AI",
|
"model": "Modello AI",
|
||||||
"temperature": "Creatività della risposta (0-2)",
|
"temperature": "Creatività della risposta (0-2)",
|
||||||
"max_tokens": "Lunghezza massima della risposta (1-4096)",
|
"max_tokens": "Lunghezza massima della risposta (1-100000)",
|
||||||
"request_interval": "Intervallo minimo di richiesta (0.1-60 secondi)",
|
"request_interval": "Intervallo minimo di richiesta (0.1-60 secondi)",
|
||||||
"context_messages": "Numero di messaggi precedenti da includere nel contesto (1-20)",
|
"context_messages": "Numero di messaggi precedenti da includere nel contesto (1-20)",
|
||||||
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
|
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
|
||||||
@@ -89,14 +89,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (compatibile)",
|
"openai": "OpenAI (compatibile)",
|
||||||
"anthropic": "Anthropic (compatibile)"
|
"anthropic": "Anthropic (compatibile)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Fai una domanda (HA Text AI)",
|
"name": "Fai una domanda (HA Text AI)",
|
||||||
"description": "Invia una domanda al modello AI e ricevi una risposta dettagliata. La risposta sarà memorizzata nella cronologia delle conversazioni e potrà essere recuperata in seguito.",
|
"description": "Invia una domanda al modello AI e ricevi una risposta dettagliata. Questo servizio ora restituisce i dati di risposta direttamente, eliminando la necessità di sensori di testo separati e la limitazione di 255 caratteri. La risposta sarà anche memorizzata nella cronologia delle conversazioni.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Istanze",
|
"name": "Istanze",
|
||||||
@@ -124,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Token massimi",
|
"name": "Token massimi",
|
||||||
"description": "Lunghezza massima della risposta (1-4096 token)"
|
"description": "Lunghezza massima della risposta (1-100000 token)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"model": "Модель ИИ для использования",
|
"model": "Модель ИИ для использования",
|
||||||
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
|
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
|
||||||
"temperature": "Креативность ответа (0-2, меньше = более сфокусированно)",
|
"temperature": "Креативность ответа (0-2, меньше = более сфокусированно)",
|
||||||
"max_tokens": "Максимальная длина ответа (1-4096 токенов)",
|
"max_tokens": "Максимальная длина ответа (1-100000 токенов)",
|
||||||
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
||||||
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)",
|
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)",
|
||||||
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"api_key": "API-ключ для аутентификации",
|
"api_key": "API-ключ для аутентификации",
|
||||||
"model": "Модель ИИ для использования",
|
"model": "Модель ИИ для использования",
|
||||||
"temperature": "Креативность ответа (0-2, меньше = более сфокусированно)",
|
"temperature": "Креативность ответа (0-2, меньше = более сфокусированно)",
|
||||||
"max_tokens": "Максимальная длина ответа (1-4096 токенов)",
|
"max_tokens": "Максимальная длина ответа (1-100000 токенов)",
|
||||||
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
|
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
|
||||||
"api_provider": "Провайдер API",
|
"api_provider": "Провайдер API",
|
||||||
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "Модель ИИ",
|
"model": "Модель ИИ",
|
||||||
"temperature": "Креативность ответа (0-2)",
|
"temperature": "Креативность ответа (0-2)",
|
||||||
"max_tokens": "Максимальная длина ответа (1-4096)",
|
"max_tokens": "Максимальная длина ответа (1-100000)",
|
||||||
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
||||||
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)",
|
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)",
|
||||||
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
||||||
@@ -89,14 +89,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (совместимый)",
|
"openai": "OpenAI (совместимый)",
|
||||||
"anthropic": "Anthropic (совместимый)"
|
"anthropic": "Anthropic (совместимый)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Задать вопрос (Текстовый ИИ HA)",
|
"name": "Задать вопрос (HA Text AI)",
|
||||||
"description": "Отправить вопрос модели ИИ и получить подробный ответ. Ответ будет сохранен в истории разговора и может быть получен позже.",
|
"description": "Отправить вопрос модели ИИ и получить подробный ответ. Сервис теперь возвращает данные ответа напрямую, устраняя необходимость в отдельных текстовых сенсорах и ограничение в 255 символов. Ответ также будет сохранен в истории разговора.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Экземпляр",
|
"name": "Экземпляр",
|
||||||
@@ -124,7 +126,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Максимум токенов",
|
"name": "Максимум токенов",
|
||||||
"description": "Максимальная длина ответа (1-4096 токенов)"
|
"description": "Максимальная длина ответа (1-100000 токенов)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
|
||||||
"title": "Изаберите AI провајдера",
|
|
||||||
"description": "Изаберите који AI сервис провајдер да користите за ову инстанцу.",
|
|
||||||
"data": {
|
|
||||||
"api_provider": "API провајдер",
|
|
||||||
"context_messages": "Број контекстуалних порука које треба задржати (1-20)",
|
|
||||||
"max_history_size": "Максимална величина историје разговора (1-100)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "Подешавања провајдера",
|
"title": "Подешавања провајдера",
|
||||||
"description": "Обезбедите детаље о вези за изабраног AI провајдера.",
|
"description": "Обезбедите детаље о вези за изабраног AI провајдера.",
|
||||||
@@ -19,7 +10,7 @@
|
|||||||
"model": "AI модел који ће се користити",
|
"model": "AI модел који ће се користити",
|
||||||
"api_endpoint": "Прилагођени URL API крајње тачке (опционо)",
|
"api_endpoint": "Прилагођени URL API крајње тачке (опционо)",
|
||||||
"temperature": "Креативност одговора (0-2, нижа = фокусираније)",
|
"temperature": "Креативност одговора (0-2, нижа = фокусираније)",
|
||||||
"max_tokens": "Максимална дужина одговора (1-4096 токена)",
|
"max_tokens": "Максимална дужина одговора (1-100000 токена)",
|
||||||
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
||||||
"context_messages": "Број контекстуалних порука које треба задржати (1-20)",
|
"context_messages": "Број контекстуалних порука које треба задржати (1-20)",
|
||||||
"max_history_size": "Максимална величина историје разговора (1-100)"
|
"max_history_size": "Максимална величина историје разговора (1-100)"
|
||||||
@@ -33,7 +24,7 @@
|
|||||||
"api_key": "API кључ за аутентификацију",
|
"api_key": "API кључ за аутентификацију",
|
||||||
"model": "AI модел који ће се користити",
|
"model": "AI модел који ће се користити",
|
||||||
"temperature": "Креативност одговора (0-2, нижа = фокусираније)",
|
"temperature": "Креативност одговора (0-2, нижа = фокусираније)",
|
||||||
"max_tokens": "Максимална дужина одговора (1-4096 токена)",
|
"max_tokens": "Максимална дужина одговора (1-100000 токена)",
|
||||||
"api_endpoint": "Прилагођени URL API крајње тачке (опционо)",
|
"api_endpoint": "Прилагођени URL API крајње тачке (опционо)",
|
||||||
"api_provider": "API провајдер",
|
"api_provider": "API провајдер",
|
||||||
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
||||||
@@ -77,7 +68,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "AI модел",
|
"model": "AI модел",
|
||||||
"temperature": "Креативност одговора (0-2)",
|
"temperature": "Креативност одговора (0-2)",
|
||||||
"max_tokens": "Максимална дужина одговора (1-4096)",
|
"max_tokens": "Максимална дужина одговора (1-100000)",
|
||||||
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
||||||
"context_messages": "Број претходних порука које треба укључити у контекст (1-20)",
|
"context_messages": "Број претходних порука које треба укључити у контекст (1-20)",
|
||||||
"max_history_size": "Максимална величина историје разговора (1-100)"
|
"max_history_size": "Максимална величина историје разговора (1-100)"
|
||||||
@@ -89,14 +80,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI (компатибилан)",
|
"openai": "OpenAI (компатибилан)",
|
||||||
"anthropic": "Anthropic (компатибилан)"
|
"anthropic": "Anthropic (компатибилан)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Поставите питање (HA Text AI)",
|
"name": "Поставите питање (HA Text AI)",
|
||||||
"description": "Пошаљите питање AI моделу и примите детаљан одговор. Одговор ће бити сачуван у историји разговора и може се касније повратити.",
|
"description": "Пошаљите питање AI моделу и добијте детаљан одговор. Овај сервис сада враћа податке одговора директно, елиминишући потребу за засебним текстуалним сензорима и ограничење од 255 карактера. Одговор ће такође бити сачуван у историји разговора.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Инстанца",
|
"name": "Инстанца",
|
||||||
@@ -124,7 +117,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "Максимални токени",
|
"name": "Максимални токени",
|
||||||
"description": "Максимална дужина одговора (1-4096 токена)"
|
"description": "Максимална дужина одговора (1-100000 токена)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
|
||||||
"title": "选择AI提供者",
|
|
||||||
"description": "选择要用于此实例的AI服务提供者。",
|
|
||||||
"data": {
|
|
||||||
"api_provider": "API提供者",
|
|
||||||
"context_messages": "保留的上下文消息数量(1-20)",
|
|
||||||
"max_history_size": "最大对话历史大小(1-100)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "提供者设置",
|
"title": "提供者设置",
|
||||||
"description": "提供所选AI提供者的连接详细信息。",
|
"description": "提供所选AI提供者的连接详细信息。",
|
||||||
@@ -19,7 +10,7 @@
|
|||||||
"model": "要使用的AI模型",
|
"model": "要使用的AI模型",
|
||||||
"api_endpoint": "自定义API端点URL(可选)",
|
"api_endpoint": "自定义API端点URL(可选)",
|
||||||
"temperature": "响应创造力(0-2,越低越专注)",
|
"temperature": "响应创造力(0-2,越低越专注)",
|
||||||
"max_tokens": "最大响应长度(1-4096个标记)",
|
"max_tokens": "最大响应长度(1-100000个标记)",
|
||||||
"request_interval": "请求之间的最小时间(0.1-60秒)",
|
"request_interval": "请求之间的最小时间(0.1-60秒)",
|
||||||
"context_messages": "保留的上下文消息数量(1-20)",
|
"context_messages": "保留的上下文消息数量(1-20)",
|
||||||
"max_history_size": "最大对话历史大小(1-100)"
|
"max_history_size": "最大对话历史大小(1-100)"
|
||||||
@@ -33,7 +24,7 @@
|
|||||||
"api_key": "用于身份验证的API密钥",
|
"api_key": "用于身份验证的API密钥",
|
||||||
"model": "要使用的AI模型",
|
"model": "要使用的AI模型",
|
||||||
"temperature": "响应创造力(0-2,越低越专注)",
|
"temperature": "响应创造力(0-2,越低越专注)",
|
||||||
"max_tokens": "最大响应长度(1-4096个标记)",
|
"max_tokens": "最大响应长度(1-100000个标记)",
|
||||||
"api_endpoint": "自定义API端点URL(可选)",
|
"api_endpoint": "自定义API端点URL(可选)",
|
||||||
"api_provider": "API提供者",
|
"api_provider": "API提供者",
|
||||||
"request_interval": "请求之间的最小时间(0.1-60秒)",
|
"request_interval": "请求之间的最小时间(0.1-60秒)",
|
||||||
@@ -77,7 +68,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"model": "AI模型",
|
"model": "AI模型",
|
||||||
"temperature": "响应创造力(0-2)",
|
"temperature": "响应创造力(0-2)",
|
||||||
"max_tokens": "最大响应长度(1-4096)",
|
"max_tokens": "最大响应长度(1-100000)",
|
||||||
"request_interval": "最小请求间隔(0.1-60秒)",
|
"request_interval": "最小请求间隔(0.1-60秒)",
|
||||||
"context_messages": "要包含在上下文中的先前消息数量(1-20)",
|
"context_messages": "要包含在上下文中的先前消息数量(1-20)",
|
||||||
"max_history_size": "最大对话历史大小(1-100)"
|
"max_history_size": "最大对话历史大小(1-100)"
|
||||||
@@ -89,14 +80,16 @@
|
|||||||
"api_provider": {
|
"api_provider": {
|
||||||
"options": {
|
"options": {
|
||||||
"openai": "OpenAI(兼容)",
|
"openai": "OpenAI(兼容)",
|
||||||
"anthropic": "Anthropic(兼容)"
|
"anthropic": "Anthropic(兼容)",
|
||||||
|
"deepseek": "DeepSeek",
|
||||||
|
"gemini": "Google Gemini"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "提问(HA文本AI)",
|
"name": "提问 (HA Text AI)",
|
||||||
"description": "向AI模型发送问题并接收详细响应。响应将存储在对话历史中,可以稍后检索。",
|
"description": "向AI模型发送问题并获得详细回答。此服务现在直接返回响应数据,消除了对单独文本传感器的需要和255字符限制。响应也将存储在对话历史中。",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "实例",
|
"name": "实例",
|
||||||
@@ -124,7 +117,7 @@
|
|||||||
},
|
},
|
||||||
"max_tokens": {
|
"max_tokens": {
|
||||||
"name": "最大标记数",
|
"name": "最大标记数",
|
||||||
"description": "响应的最大长度(1-4096个标记)"
|
"description": "响应的最大长度(1-100000个标记)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "HA text AI",
|
"name": "HA Text AI",
|
||||||
"render_readme": true,
|
"render_readme": true,
|
||||||
"homeassistant": "2024.11.0"
|
"homeassistant": "2024.12.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
```
|
```
|
||||||
ha-text-ai/
|
ha_text_ai/
|
||||||
│
|
├── __init__.py
|
||||||
├── custom_components/
|
├── api_client.py
|
||||||
├── ha_text_ai/
|
├── config_flow.py
|
||||||
│ ├── __init__.py
|
├── const.py
|
||||||
│ ├── config_flow.py
|
├── coordinator.py
|
||||||
│ ├── coordinator.py
|
├── icons
|
||||||
│ ├── manifest.json
|
│ ├── dark_icon.png
|
||||||
│ ├── sensor.py
|
│ ├── dark_icon@2x.png
|
||||||
│ ├── services.yaml
|
│ ├── dark_logo.png
|
||||||
│ ├── const.py
|
│ ├── dark_logo@2x.png
|
||||||
│ └── api_client.py
|
│ ├── icon.png
|
||||||
│
|
│ ├── icon@2x.png
|
||||||
├── translations/
|
│ ├── logo.png
|
||||||
│ ├── en.json
|
│ └── logo@2x.png
|
||||||
│ ├── de.json
|
├── manifest.json
|
||||||
│ └── ru.json
|
├── sensor.py
|
||||||
│
|
├── services.yaml
|
||||||
└── icons/
|
└── translations
|
||||||
├── icon.png
|
├── de.json
|
||||||
├── icon@2x.png
|
├── en.json
|
||||||
├── logo.png
|
├── es.json
|
||||||
└── logo@2x.png
|
├── hi.json
|
||||||
|
├── it.json
|
||||||
|
├── ru.json
|
||||||
|
├── sr.json
|
||||||
|
└── zh.json
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||