mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-22 07:03:58 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73788373cd | ||
|
|
4bfc96019b | ||
|
|
2138fc7654 | ||
|
|
95bd2ebb41 | ||
|
|
cad0fd7031 | ||
|
|
c003b258f6 | ||
|
|
65a10c77f4 | ||
|
|
e1463828c9 |
-41
@@ -1,41 +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
|
||||
*.pdf
|
||||
@@ -106,12 +106,13 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
- Home Assistant 2024.11 or later
|
||||
- Home Assistant 2024.2.2 or later
|
||||
- Active API key from:
|
||||
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
|
||||
- Anthropic ([Get key](https://console.anthropic.com/))
|
||||
- DeepSeek 🆕 ([Get key](https://platform.deepseek.com/api_keys))
|
||||
- DeepSeek ([Get key](https://platform.deepseek.com/api_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
|
||||
- Stable internet connection
|
||||
@@ -159,6 +160,9 @@ To be compatible, a provider should support:
|
||||
## ⚡ Installation
|
||||
|
||||
### 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>
|
||||
1. Open HACS in Home Assistant
|
||||
2. Click on "Integrations"
|
||||
@@ -169,8 +173,6 @@ To be compatible, a provider should support:
|
||||
7. Click "Download"
|
||||
8. Restart Home Assistant
|
||||
|
||||
Note: Also Integration has been submitted to HACS store and is currently pending review in [pull request #2896](https://github.com/hacs/default/pull/2896).
|
||||
|
||||
### Manual Installation
|
||||
1. Download the latest release
|
||||
2. Extract and copy `custom_components/ha_text_ai` to your `custom_components` directory
|
||||
|
||||
@@ -40,13 +40,16 @@ from .const import (
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
API_PROVIDER_DEEPSEEK,
|
||||
API_PROVIDER_GEMINI,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_DEEPSEEK_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_OPENAI_ENDPOINT,
|
||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||
DEFAULT_DEEPSEEK_ENDPOINT,
|
||||
DEFAULT_GEMINI_ENDPOINT,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
DEFAULT_CONTEXT_MESSAGES,
|
||||
API_TIMEOUT,
|
||||
|
||||
@@ -20,6 +20,7 @@ from .const import (
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
API_PROVIDER_DEEPSEEK,
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_GEMINI,
|
||||
MIN_TEMPERATURE,
|
||||
MAX_TEMPERATURE,
|
||||
MIN_MAX_TOKENS,
|
||||
@@ -115,6 +116,10 @@ class APIClient:
|
||||
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:
|
||||
return await self._create_openai_completion(
|
||||
model, messages, temperature, max_tokens
|
||||
@@ -238,6 +243,57 @@ class APIClient:
|
||||
_LOGGER.error(f"Connection check failed: {str(e)}")
|
||||
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."""
|
||||
# Extract API key from headers (Bearer token)
|
||||
api_key = self.headers.get("Authorization", "").replace("Bearer ", "")
|
||||
url = f"{self.endpoint}/models/{model}:generateContent?key={api_key}"
|
||||
|
||||
# Convert messages to Gemini format
|
||||
contents = []
|
||||
system_instruction = ""
|
||||
|
||||
for msg in messages:
|
||||
if msg['role'] == 'system':
|
||||
system_instruction += msg['content'] + "\n"
|
||||
else:
|
||||
contents.append({
|
||||
"role": "user" if msg['role'] == 'user' else "model",
|
||||
"parts": [{"text": msg['content']}]
|
||||
})
|
||||
|
||||
payload = {
|
||||
"contents": contents,
|
||||
"generationConfig": {
|
||||
"temperature": temperature,
|
||||
"maxOutputTokens": max_tokens
|
||||
}
|
||||
}
|
||||
if system_instruction:
|
||||
payload["systemInstruction"] = {
|
||||
"parts": [{"text": system_instruction}]
|
||||
}
|
||||
|
||||
data = await self._make_request(url, payload)
|
||||
return {
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": data["candidates"][0]["content"]["parts"][0]["text"]
|
||||
}
|
||||
}],
|
||||
"usage": {
|
||||
"prompt_tokens": data["usageMetadata"]["promptTokenCount"],
|
||||
"completion_tokens": data["usageMetadata"]["candidatesTokenCount"],
|
||||
"total_tokens": data["usageMetadata"]["totalTokenCount"]
|
||||
}
|
||||
}
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown API client."""
|
||||
_LOGGER.debug("Shutting down API client")
|
||||
|
||||
@@ -29,15 +29,18 @@ from .const import (
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
API_PROVIDER_DEEPSEEK,
|
||||
API_PROVIDER_GEMINI,
|
||||
API_PROVIDERS,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_DEEPSEEK_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
DEFAULT_OPENAI_ENDPOINT,
|
||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||
DEFAULT_DEEPSEEK_ENDPOINT,
|
||||
DEFAULT_GEMINI_ENDPOINT,
|
||||
DEFAULT_CONTEXT_MESSAGES,
|
||||
MIN_TEMPERATURE,
|
||||
MAX_TEMPERATURE,
|
||||
@@ -93,15 +96,20 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._errors = {}
|
||||
|
||||
if user_input is None:
|
||||
# Выбор endpoint по провайдеру
|
||||
# Selecting an endpoint by provider
|
||||
default_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)
|
||||
|
||||
# Выбор модели по умолчанию по провайдеру
|
||||
default_model = DEFAULT_DEEPSEEK_MODEL if self._provider == API_PROVIDER_DEEPSEEK else DEFAULT_MODEL
|
||||
# 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(
|
||||
step_id="provider",
|
||||
@@ -256,7 +264,11 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
unique_id = f"{DOMAIN}_{normalized_name}_{self._provider}".lower()
|
||||
|
||||
default_model = DEFAULT_DEEPSEEK_MODEL if self._provider == API_PROVIDER_DEEPSEEK else DEFAULT_MODEL
|
||||
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 = {
|
||||
CONF_API_PROVIDER: self._provider,
|
||||
@@ -304,6 +316,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
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(
|
||||
step_id="init",
|
||||
|
||||
@@ -12,6 +12,8 @@ from typing import Final
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
import logging
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Domain and platforms
|
||||
DOMAIN: Final = "ha_text_ai"
|
||||
@@ -22,11 +24,13 @@ CONF_API_PROVIDER: Final = "api_provider"
|
||||
API_PROVIDER_OPENAI: Final = "openai"
|
||||
API_PROVIDER_ANTHROPIC: Final = "anthropic"
|
||||
API_PROVIDER_DEEPSEEK: Final = "deepseek"
|
||||
API_PROVIDER_GEMINI: Final = "gemini"
|
||||
|
||||
API_PROVIDERS: Final = [
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
API_PROVIDER_DEEPSEEK
|
||||
API_PROVIDER_DEEPSEEK,
|
||||
API_PROVIDER_GEMINI
|
||||
]
|
||||
|
||||
# Read version from manifest.json
|
||||
@@ -49,6 +53,7 @@ except Exception as err:
|
||||
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
|
||||
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
|
||||
CONF_MODEL: Final = "model"
|
||||
@@ -69,6 +74,7 @@ ICONS_SUBDOMAIN = "icons"
|
||||
# Default values
|
||||
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
||||
DEFAULT_DEEPSEEK_MODEL: Final = "deepseek-chat"
|
||||
DEFAULT_GEMINI_MODEL: Final = "gemini-pro"
|
||||
DEFAULT_TEMPERATURE: Final = 0.1
|
||||
DEFAULT_MAX_TOKENS: Final = 1000
|
||||
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"requirements": [
|
||||
"openai>=1.12.0",
|
||||
"anthropic>=0.8.0",
|
||||
"google-generativeai>=0.3.0",
|
||||
"aiohttp>=3.8.0",
|
||||
"async-timeout>=4.0.0",
|
||||
"certifi>=2024.2.2"
|
||||
@@ -23,6 +24,6 @@
|
||||
"single_config_entry": false,
|
||||
"ssdp": [],
|
||||
"usb": [],
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.3",
|
||||
"zeroconf": []
|
||||
}
|
||||
|
||||
@@ -88,12 +88,13 @@
|
||||
"selector": {
|
||||
"api_provider": {
|
||||
"options": {
|
||||
"openai": "OpenAI (kompatibel)",
|
||||
"anthropic": "Anthropic (kompatibel)",
|
||||
"deepseek": "DeepSeek"
|
||||
}
|
||||
"openai": "OpenAI (compatible)",
|
||||
"anthropic": "Anthropic (compatible)",
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"name": "Frage stellen (HA Text AI)",
|
||||
|
||||
@@ -86,11 +86,12 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"api_provider": {
|
||||
"options": {
|
||||
"openai": "OpenAI (compatible)",
|
||||
"anthropic": "Anthropic (compatible)",
|
||||
"deepseek": "DeepSeek"
|
||||
"api_provider": {
|
||||
"options": {
|
||||
"openai": "OpenAI (compatible)",
|
||||
"anthropic": "Anthropic (compatible)",
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,10 +90,11 @@
|
||||
"options": {
|
||||
"openai": "OpenAI (compatible)",
|
||||
"anthropic": "Anthropic (compatible)",
|
||||
"deepseek": "DeepSeek"
|
||||
}
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"name": "Hacer Pregunta (HA Text AI)",
|
||||
|
||||
@@ -90,7 +90,8 @@
|
||||
"options": {
|
||||
"openai": "OpenAI (अनुकूलित)",
|
||||
"anthropic": "Anthropic (अनुकूलित)",
|
||||
"deepseek": "DeepSeek"
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,7 +90,8 @@
|
||||
"options": {
|
||||
"openai": "OpenAI (compatibile)",
|
||||
"anthropic": "Anthropic (compatibile)",
|
||||
"deepseek": "DeepSeek"
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,7 +90,8 @@
|
||||
"options": {
|
||||
"openai": "OpenAI (совместимый)",
|
||||
"anthropic": "Anthropic (совместимый)",
|
||||
"deepseek": "DeepSeek"
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,7 +90,8 @@
|
||||
"options": {
|
||||
"openai": "OpenAI (компатибилан)",
|
||||
"anthropic": "Anthropic (компатибилан)",
|
||||
"deepseek": "DeepSeek"
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,7 +90,8 @@
|
||||
"options": {
|
||||
"openai": "OpenAI(兼容)",
|
||||
"anthropic": "Anthropic(兼容)",
|
||||
"deepseek": "DeepSeek "
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user