Initial Home Assistant commit

This commit is contained in:
2026-06-05 22:34:31 -04:00
commit 6a58b10e6c
4494 changed files with 297833 additions and 0 deletions
@@ -0,0 +1,202 @@
"""The AI Automation Suggester integration."""
import logging
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady, ServiceValidationError
from homeassistant.helpers.typing import ConfigType
from .api import async_register_http_views
from .const import (
DOMAIN,
PLATFORMS,
CONF_PROVIDER,
SERVICE_CLEAR_HISTORY,
SERVICE_GENERATE_SUGGESTIONS,
SERVICE_UPDATE_SUGGESTION,
ATTR_PROVIDER_CONFIG,
ATTR_CUSTOM_PROMPT,
CONFIG_VERSION
)
from .coordinator import AIAutomationCoordinator
from .store import async_get_suggestion_store
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old config entry if necessary."""
_LOGGER.debug(f"async_migrate_entry {config_entry.version}")
# Currently, no migration logic beyond ensuring version matches CONFIG_VERSION
if config_entry.version < CONFIG_VERSION:
_LOGGER.debug(f"Migrating config entry from version {config_entry.version} to {CONFIG_VERSION}")
new_data = {**config_entry.data}
new_data.pop('scan_frequency', None)
new_data.pop('initial_lag_time', None)
hass.config_entries.async_update_entry(config_entry, data=new_data, version=CONFIG_VERSION)
_LOGGER.debug("Migration successful")
return True
return True
def _listish(value):
"""Schema helper for service fields that can be a CSV string, list, or object."""
if value is None:
return []
if isinstance(value, (str, list, tuple, dict)):
return value
raise vol.Invalid("expected a list, comma-separated string, or object")
GENERATE_SUGGESTIONS_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_PROVIDER_CONFIG): str,
vol.Optional(ATTR_CUSTOM_PROMPT): str,
vol.Optional("all_entities", default=False): bool,
vol.Optional("domains", default=[]): _listish,
vol.Optional("exclude_domains", default=[]): _listish,
vol.Optional("exclude_entities", default=[]): _listish,
vol.Optional("exclude_areas", default=[]): _listish,
vol.Optional("entity_limit", default=200): vol.All(vol.Coerce(int), vol.Range(min=1, max=2000)),
vol.Optional("automation_read_yaml", default=False): bool,
vol.Optional("automation_limit", default=100): vol.All(vol.Coerce(int), vol.Range(min=0, max=1000)),
}
)
UPDATE_SUGGESTION_SCHEMA = vol.Schema(
{
vol.Required("suggestion_id"): str,
vol.Required("status"): vol.In(["accepted", "declined", "dismissed", "new"]),
}
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the AI Automation Suggester component."""
hass.data.setdefault(DOMAIN, {})
async_register_http_views(hass)
async def handle_generate_suggestions(call: ServiceCall) -> None:
"""Handle the generate_suggestions service call."""
try:
coordinator = None
provider_config = call.data.get(ATTR_PROVIDER_CONFIG)
if provider_config:
coordinator = hass.data[DOMAIN].get(provider_config)
else:
# Find first available coordinator if none specified
for entry_id, coord in hass.data[DOMAIN].items():
if isinstance(coord, AIAutomationCoordinator):
coordinator = coord
break
if coordinator is None:
raise ServiceValidationError("No AI Automation Suggester provider configured")
await coordinator.async_generate_suggestions(
custom_prompt=call.data.get(ATTR_CUSTOM_PROMPT),
all_entities=call.data.get("all_entities", False),
domains=call.data.get("domains", []),
exclude_domains=call.data.get("exclude_domains", []),
exclude_entities=call.data.get("exclude_entities", []),
exclude_areas=call.data.get("exclude_areas", []),
entity_limit=call.data.get("entity_limit", 200),
automation_read_yaml=call.data.get("automation_read_yaml", False),
automation_limit=call.data.get("automation_limit", 100),
)
except KeyError:
raise ServiceValidationError("Provider configuration not found")
except Exception as err:
raise ServiceValidationError(f"Failed to generate suggestions: {err}")
async def handle_clear_history(call: ServiceCall) -> None:
"""Clear stored suggestion history."""
await async_get_suggestion_store(hass).async_clear()
async def handle_update_suggestion(call: ServiceCall) -> None:
"""Update a stored suggestion status."""
suggestion = await async_get_suggestion_store(hass).async_update_status(
call.data["suggestion_id"], call.data["status"]
)
if suggestion is None:
raise ServiceValidationError("Suggestion not found")
# Register the service
hass.services.async_register(
DOMAIN,
SERVICE_GENERATE_SUGGESTIONS,
handle_generate_suggestions,
schema=GENERATE_SUGGESTIONS_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_CLEAR_HISTORY,
handle_clear_history,
)
hass.services.async_register(
DOMAIN,
SERVICE_UPDATE_SUGGESTION,
handle_update_suggestion,
schema=UPDATE_SUGGESTION_SCHEMA,
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up AI Automation Suggester from a config entry."""
try:
if CONF_PROVIDER not in entry.data:
raise ConfigEntryNotReady("Provider not specified in config")
coordinator = AIAutomationCoordinator(hass, entry)
hass.data[DOMAIN][entry.entry_id] = coordinator
# Use the new async_forward_entry_setups method (plural) instead of the deprecated async_forward_entry_setup.
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
_LOGGER.debug(
"Setup complete for %s with provider %s",
entry.title,
entry.data.get(CONF_PROVIDER)
)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
@callback
def handle_custom_event(event):
_LOGGER.debug("Received custom event '%s', triggering suggestions with all_entities=True", event.event_type)
hass.async_create_task(coordinator_request_all_suggestions())
async def coordinator_request_all_suggestions():
await coordinator.async_generate_suggestions(all_entities=True)
entry.async_on_unload(hass.bus.async_listen("ai_automation_suggester_update", handle_custom_event))
return True
except Exception as err:
_LOGGER.error("Failed to setup integration: %s", err)
raise ConfigEntryNotReady from err
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
try:
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
coordinator = hass.data[DOMAIN].pop(entry.entry_id)
await coordinator.async_shutdown()
return unload_ok
except Exception as err:
_LOGGER.error("Error unloading entry: %s", err)
return False
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)
@@ -0,0 +1,53 @@
"""HTTP API endpoints for stored automation suggestions."""
from __future__ import annotations
from aiohttp import web
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant
from .store import async_get_suggestion_store
class AISuggestionsView(HomeAssistantView):
"""Return stored suggestions for Lovelace cards and dashboards."""
url = "/api/ai_automation_suggester/suggestions"
name = "api:ai_automation_suggester:suggestions"
requires_auth = True
async def get(self, request: web.Request) -> web.Response:
hass: HomeAssistant = request.app["hass"]
store = async_get_suggestion_store(hass)
return self.json(await store.async_list())
class AISuggestionActionView(HomeAssistantView):
"""Update a suggestion action from the existing custom card."""
url = "/api/ai_automation_suggester/{action}/{suggestion_id}"
name = "api:ai_automation_suggester:suggestion_action"
requires_auth = True
async def post(self, request: web.Request, action: str, suggestion_id: str) -> web.Response:
status_map = {
"accept": "accepted",
"decline": "declined",
"dismiss": "dismissed",
}
if action not in status_map:
return self.json({"success": False, "error": "Unsupported action"}, status_code=400)
hass: HomeAssistant = request.app["hass"]
store = async_get_suggestion_store(hass)
suggestion = await store.async_update_status(suggestion_id, status_map[action])
if suggestion is None:
return self.json({"success": False, "error": "Suggestion not found"}, status_code=404)
return self.json({"success": True, "suggestion": suggestion})
def async_register_http_views(hass: HomeAssistant) -> None:
"""Register HTTP API views."""
hass.http.register_view(AISuggestionsView())
hass.http.register_view(AISuggestionActionView())
@@ -0,0 +1,48 @@
########################################################################################
# AI Suggestions - New Entity Detection
########################################################################################
# This automation triggers the AI Automation Suggester whenever new entities are added to
# your Home Assistant instance.
#
# CUSTOMIZATION OPTIONS:
# 1. Trigger Events:
# - Currently monitors both 'create' and 'update' events
# - Remove the second trigger if you only want new entity detection
#
# 2. Throttling:
# - Uses a cooldown of 1 hour to prevent excessive API calls
# - Adjust the hours value in the condition template if needed
########################################################################################
alias: "AI Suggestions - New Entity Detection"
description: "Generates automation suggestions whenever new entities are registered in Home Assistant"
trigger:
# Trigger when a new entity is created
- platform: event
event_type: entity_registry_updated
event_data:
action: create
# Optional: Trigger when an entity is updated
- platform: event
event_type: entity_registry_updated
event_data:
action: update
condition:
# Simple throttling based on last trigger time
- condition: template
value_template: >-
{% set automation = states.automation.ai_suggestions_new_entity_detection %}
{% if automation and automation.attributes.last_triggered %}
{% set hours_since = ((now() - as_datetime(automation.attributes.last_triggered)).total_seconds() / 3600) | float %}
{{ hours_since > 1.0 }}
{% else %}
true
{% endif %}
action:
- service: ai_automation_suggester.generate_suggestions
target: {}
data: {}
@@ -0,0 +1,39 @@
########################################################################################
# AI Suggestions - Weekly System Review
########################################################################################
# This automation performs a comprehensive weekly review of your Home Assistant setup
# to suggest new automation opportunities and improvements.
#
# CUSTOMIZATION OPTIONS:
# 1. Schedule:
# - Currently runs at 3 AM on Sundays
# - Modify the 'at' field to change the time
# - Change the weekday condition to run on different days
########################################################################################
alias: "AI Suggestions - Weekly Review"
description: "Performs a weekly scan of all entities to suggest new automation opportunities"
trigger:
# Runs at 3 AM
- platform: time
at: "03:00:00"
condition:
# Only runs on Sundays
- condition: time
weekday:
- sun
action:
# Generate suggestions using the configured provider(s), scanning all entities
- service: ai_automation_suggester.generate_suggestions
data:
all_entities: true
# Create a notification
- service: persistent_notification.create
data:
title: "Weekly Automation Review"
message: "The AI Automation Suggester has completed its weekly review. Check the suggestions sensor for new automation ideas!"
notification_id: "weekly_automation_review"
@@ -0,0 +1,690 @@
# custom_components/ai_automation_suggester/config_flow.py
"""Config flow for AI Automation Suggester."""
from __future__ import annotations
import logging
from typing import Any, Dict, Optional
import aiohttp
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import TextSelector, TextSelectorConfig
from .const import *
from .endpoint_utils import (
bearer_auth_headers,
ensure_http_url,
ollama_api_candidates,
ollama_base_url,
openai_model_endpoint_candidates,
)
_LOGGER = logging.getLogger(__name__)
# ─────────────────────────────────────────────────────────────
# Lightweight provider validators (unchanged)
# ─────────────────────────────────────────────────────────────
class ProviderValidator:
"""Ping each provider with a dummy request to validate credentials."""
def __init__(self, hass, request_timeout: int | None = None):
self.session = async_get_clientsession(hass)
self.timeout = aiohttp.ClientTimeout(total=max(10, int(request_timeout or DEFAULT_REQUEST_TIMEOUT)))
async def validate_openai(self, api_key: str) -> Optional[str]:
hdr = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
try:
resp = await self.session.get("https://api.openai.com/v1/models", headers=hdr, timeout=self.timeout)
return None if resp.status == 200 else await resp.text()
except Exception as err: # noqa: BLE001
return str(err)
async def validate_anthropic(self, api_key: str, model: str) -> Optional[str]:
hdr = {
"x-api-key": api_key,
"anthropic-version": VERSION_ANTHROPIC,
"content-type": "application/json",
}
try:
resp = await self.session.get("https://api.anthropic.com/v1/models", headers=hdr, timeout=self.timeout)
return None if resp.status == 200 else await resp.text()
except Exception as err:
return str(err)
async def validate_google(self, api_key: str, model: str) -> Optional[str]:
try:
resp = await self.session.get(
f"https://generativelanguage.googleapis.com/v1beta/models/{model}?key={api_key}",
timeout=self.timeout,
)
return None if resp.status == 200 else await resp.text()
except Exception as err:
return str(err)
async def validate_groq(self, api_key: str) -> Optional[str]:
hdr = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
try:
resp = await self.session.get("https://api.groq.com/openai/v1/models", headers=hdr, timeout=self.timeout)
return None if resp.status == 200 else await resp.text()
except Exception as err:
return str(err)
async def validate_localai(self, ip: str, port: int, https: bool) -> Optional[str]:
proto = "https" if https else "http"
try:
resp = await self.session.get(f"{proto}://{ip}:{port}/v1/models", timeout=self.timeout)
return None if resp.status == 200 else await resp.text()
except Exception as err:
return str(err)
async def validate_ollama(
self,
ip: str | None,
port: int | None,
https: bool,
base_url: str | None = None,
api_key: str | None = None,
) -> Optional[str]:
base = ollama_base_url(base_url=base_url, ip_address=ip, port=port, https=https)
if not base:
return "Ollama host/port or base URL is required"
last_error = None
headers = bearer_auth_headers(api_key)
try:
for endpoint in ollama_api_candidates(base, "api/tags"):
resp = await self.session.get(endpoint, headers=headers, timeout=self.timeout)
if resp.status == 200:
return None
last_error = f"{endpoint}: {resp.status} {await resp.text()}"
return last_error
except Exception as err:
return str(err)
async def validate_custom_openai(self, endpoint: str, api_key: str | None) -> Optional[str]:
hdr = {"Content-Type": "application/json"}
if api_key:
hdr["Authorization"] = f"Bearer {api_key}"
last_error = None
try:
for model_endpoint in openai_model_endpoint_candidates(endpoint):
resp = await self.session.get(model_endpoint, headers=hdr, timeout=self.timeout)
if resp.status == 200:
return None
last_error = f"{model_endpoint}: {resp.status} {await resp.text()}"
return last_error or "No valid model endpoint could be built"
except Exception as err:
return str(err)
async def validate_perplexity(self, api_key: str, model: str) -> Optional[str]:
hdr = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
# Perplexity 'sonar' models require max_tokens >= 16; a smaller value is
# rejected with a 400 during validation (issue #171).
payload = {"model": model, "messages": [{"role": "user", "content": "ping"}], "max_tokens": 16}
try:
resp = await self.session.post(ENDPOINT_PERPLEXITY, headers=hdr, json=payload, timeout=self.timeout)
return None if resp.status == 200 else await resp.text()
except Exception as err:
return str(err)
async def validate_openrouter(self, api_key: str, model: str) -> Optional[str]:
hdr = {"content-type": "application/json"}
if api_key:
hdr["Authorization"] = f"Bearer {api_key}"
try:
resp = await self.session.get(
"https://openrouter.ai/api/v1/models", headers=hdr, timeout=self.timeout
)
return None if resp.status == 200 else await resp.text()
except Exception as err:
return str(err)
async def validate_generic_openai(self, endpoint: str, api_key: str) -> Optional[str]:
hdr = {"Content-Type": "application/json"}
if api_key:
hdr["Authorization"] = f"Bearer {api_key}"
try:
resp = await self.session.get(ensure_http_url(endpoint), headers=hdr, timeout=self.timeout)
return None if resp.status == 200 else await resp.text()
except Exception as err:
return str(err)
# ─────────────────────────────────────────────────────────────
# Configflow main class
# ─────────────────────────────────────────────────────────────
class AIAutomationConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle integration setup via the UI."""
VERSION = CONFIG_VERSION
def __init__(self) -> None:
self.provider: str | None = None
self.data: Dict[str, Any] = {}
self.validator: ProviderValidator | None = None
# ───────── Initial provider choice ─────────
async def async_step_user(self, user_input: Dict[str, Any] | None = None):
errors: Dict[str, str] = {}
if user_input:
self.provider = user_input[CONF_PROVIDER]
self.data.update(user_input)
return await {
"OpenAI": self.async_step_openai,
"Anthropic": self.async_step_anthropic,
"Google": self.async_step_google,
"Groq": self.async_step_groq,
"LocalAI": self.async_step_localai,
"Ollama": self.async_step_ollama,
"Custom OpenAI": self.async_step_custom_openai,
"Mistral AI": self.async_step_mistral,
"Perplexity AI": self.async_step_perplexity,
"OpenRouter": self.async_step_openrouter,
"OpenAI Azure": self.async_step_openai_azure,
"Generic OpenAI": self.async_step_generic_openai,
"LiteLLM": self.async_step_litellm,
}[self.provider]()
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_PROVIDER): vol.In(
[
"Anthropic",
"Custom OpenAI",
"Generic OpenAI",
"Google",
"Groq",
"LiteLLM",
"LocalAI",
"Mistral AI",
"Ollama",
"OpenAI Azure",
"OpenAI",
"OpenRouter",
"Perplexity AI",
]
)
}
),
errors=errors,
)
# ───────── helper to reduce repetition ─────────
async def _provider_form(
self,
step_id: str,
schema: vol.Schema,
validate_fn,
title: str,
errors: Dict[str, str],
placeholders: Dict[str, str],
user_input: Dict[str, Any] | None,
):
if user_input:
self.validator = ProviderValidator(self.hass, user_input.get(CONF_REQUEST_TIMEOUT))
err = await validate_fn(user_input)
if err is None:
self.data.update({
**user_input,
CONF_MAX_INPUT_TOKENS: user_input.get(CONF_MAX_INPUT_TOKENS, DEFAULT_MAX_INPUT_TOKENS),
CONF_MAX_OUTPUT_TOKENS: user_input.get(CONF_MAX_OUTPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS),
})
return self.async_create_entry(title=title, data=self.data)
errors["base"] = "api_error"
placeholders["error_message"] = err
return self.async_show_form(step_id=step_id, data_schema=schema, errors=errors, description_placeholders=placeholders)
# ───────── providerspecific steps (OpenAI shown; others similar) ─────────
def _add_token_fields(self, base: Dict[Any, Any]) -> Dict[Any, Any]:
"""Append common tuning fields to the schema."""
base[vol.Optional(CONF_MAX_INPUT_TOKENS, default=DEFAULT_MAX_INPUT_TOKENS)] = vol.All(
vol.Coerce(int), vol.Range(min=100)
)
base[vol.Optional(CONF_MAX_OUTPUT_TOKENS, default=DEFAULT_MAX_OUTPUT_TOKENS)] = vol.All(
vol.Coerce(int), vol.Range(min=100)
)
base[vol.Optional(CONF_CUSTOM_SYSTEM_PROMPT, default="")] = str
base[vol.Optional(CONF_EXCLUDED_DOMAINS, default="")] = str
base[vol.Optional(CONF_EXCLUDED_ENTITIES, default="")] = str
base[vol.Optional(CONF_EXCLUDED_AREAS, default="")] = str
base[vol.Optional(CONF_HISTORY_RETENTION, default=DEFAULT_HISTORY_RETENTION)] = vol.All(
vol.Coerce(int), vol.Range(min=1, max=250)
)
base[vol.Optional(CONF_REQUEST_TIMEOUT, default=DEFAULT_REQUEST_TIMEOUT)] = vol.All(
vol.Coerce(int), vol.Range(min=10, max=1800)
)
return base
async def async_step_openai(self, user_input=None):
schema = {
vol.Required(CONF_OPENAI_API_KEY): TextSelector(TextSelectorConfig(type="password")),
vol.Optional(CONF_OPENAI_MODEL, default=DEFAULT_MODELS["OpenAI"]): str,
vol.Optional(CONF_OPENAI_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
vol.Optional(CONF_OPENAI_REASONING_EFFORT, default=DEFAULT_OPENAI_REASONING_EFFORT): vol.In(["minimal", "low", "medium", "high"]),
}
self._add_token_fields(schema)
return await self._provider_form(
"openai",
vol.Schema(schema),
lambda ui: self.validator.validate_openai(ui[CONF_OPENAI_API_KEY]),
"AI Automation Suggester (OpenAI)",
{},
{},
user_input,
)
async def async_step_anthropic(self, user_input=None):
async def _v(ui):
return await self.validator.validate_anthropic(
ui[CONF_ANTHROPIC_API_KEY], ui.get(CONF_ANTHROPIC_MODEL, DEFAULT_MODELS["Anthropic"])
)
schema = {
vol.Required(CONF_ANTHROPIC_API_KEY): TextSelector(TextSelectorConfig(type="password")),
vol.Optional(CONF_ANTHROPIC_MODEL, default=DEFAULT_MODELS["Anthropic"]): str,
vol.Optional(CONF_ANTHROPIC_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
}
self._add_token_fields(schema)
return await self._provider_form(
"anthropic",
vol.Schema(schema),
_v,
"AI Automation Suggester (Anthropic)",
{},
{},
user_input,
)
async def async_step_google(self, user_input=None):
async def _v(ui):
return await self.validator.validate_google(
ui[CONF_GOOGLE_API_KEY], ui.get(CONF_GOOGLE_MODEL, DEFAULT_MODELS["Google"])
)
schema = {
vol.Required(CONF_GOOGLE_API_KEY): TextSelector(TextSelectorConfig(type="password")),
vol.Optional(CONF_GOOGLE_MODEL, default=DEFAULT_MODELS["Google"]): str,
vol.Optional(CONF_GOOGLE_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
}
self._add_token_fields(schema)
return await self._provider_form(
"google",
vol.Schema(schema),
_v,
"AI Automation Suggester (Google)",
{},
{},
user_input,
)
async def async_step_groq(self, user_input=None):
schema = {
vol.Required(CONF_GROQ_API_KEY): TextSelector(TextSelectorConfig(type="password")),
vol.Optional(CONF_GROQ_MODEL, default=DEFAULT_MODELS["Groq"]): str,
vol.Optional(CONF_GROQ_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Range(min=0.0, max=2.0)
),
}
self._add_token_fields(schema)
return await self._provider_form(
"groq",
vol.Schema(schema),
lambda ui: self.validator.validate_groq(ui[CONF_GROQ_API_KEY]),
"AI Automation Suggester (Groq)",
{},
{},
user_input,
)
async def async_step_localai(self, user_input=None):
async def _v(ui):
return await self.validator.validate_localai(ui[CONF_LOCALAI_IP_ADDRESS], ui[CONF_LOCALAI_PORT], ui[CONF_LOCALAI_HTTPS])
schema = {
vol.Required(CONF_LOCALAI_IP_ADDRESS): str,
vol.Required(CONF_LOCALAI_PORT, default=8080): int,
vol.Required(CONF_LOCALAI_HTTPS, default=False): bool,
vol.Optional(CONF_LOCALAI_MODEL, default=DEFAULT_MODELS["LocalAI"]): str,
vol.Optional(CONF_LOCALAI_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Range(min=0.0, max=2.0)
),
}
self._add_token_fields(schema)
return await self._provider_form(
"localai",
vol.Schema(schema),
_v,
"AI Automation Suggester (LocalAI)",
{},
{},
user_input,
)
async def async_step_ollama(self, user_input=None):
async def _v(ui):
return await self.validator.validate_ollama(
ui.get(CONF_OLLAMA_IP_ADDRESS),
ui.get(CONF_OLLAMA_PORT),
ui.get(CONF_OLLAMA_HTTPS, False),
ui.get(CONF_OLLAMA_BASE_URL),
ui.get(CONF_OLLAMA_API_KEY),
)
schema = {
vol.Optional(CONF_OLLAMA_BASE_URL, default=""): str,
vol.Optional(CONF_OLLAMA_API_KEY, default=""): TextSelector(TextSelectorConfig(type="password")),
vol.Optional(CONF_OLLAMA_IP_ADDRESS, default="localhost"): str,
vol.Optional(CONF_OLLAMA_PORT, default=11434): int,
vol.Optional(CONF_OLLAMA_HTTPS, default=False): bool,
vol.Optional(CONF_OLLAMA_MODEL, default=DEFAULT_MODELS["Ollama"]): str,
vol.Optional(CONF_OLLAMA_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Range(min=0.0, max=2.0)
),
vol.Optional(CONF_OLLAMA_DISABLE_THINK, default=False): bool,
}
self._add_token_fields(schema)
return await self._provider_form(
"ollama",
vol.Schema(schema),
_v,
"AI Automation Suggester (Ollama)",
{},
{},
user_input,
)
async def async_step_custom_openai(self, user_input=None):
async def _v(ui):
return await self.validator.validate_custom_openai(ui[CONF_CUSTOM_OPENAI_ENDPOINT], ui.get(CONF_CUSTOM_OPENAI_API_KEY))
schema = {
vol.Required(CONF_CUSTOM_OPENAI_ENDPOINT): str,
vol.Optional(CONF_CUSTOM_OPENAI_API_KEY): TextSelector(TextSelectorConfig(type="password")),
vol.Optional(CONF_CUSTOM_OPENAI_MODEL, default=DEFAULT_MODELS["Custom OpenAI"]): str,
vol.Optional(CONF_CUSTOM_OPENAI_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
}
self._add_token_fields(schema)
return await self._provider_form(
"custom_openai",
vol.Schema(schema),
_v,
"AI Automation Suggester (Custom OpenAI)",
{},
{},
user_input,
)
# Mistral: no live validation needed
async def async_step_mistral(self, user_input=None):
if user_input:
self.data.update(user_input)
return self.async_create_entry(title="AI Automation Suggester (Mistral AI)", data=self.data)
schema = {
vol.Required(CONF_MISTRAL_API_KEY): TextSelector(TextSelectorConfig(type="password")),
vol.Optional(CONF_MISTRAL_MODEL, default=DEFAULT_MODELS["Mistral AI"]): str,
vol.Optional(CONF_MISTRAL_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Range(min=0.0, max=2.0)
),
}
self._add_token_fields(schema)
return self.async_show_form(step_id="mistral", data_schema=vol.Schema(schema))
async def async_step_perplexity(self, user_input=None):
async def _v(ui):
return await self.validator.validate_perplexity(
ui[CONF_PERPLEXITY_API_KEY], ui.get(CONF_PERPLEXITY_MODEL, DEFAULT_MODELS["Perplexity AI"])
)
schema = {
vol.Required(CONF_PERPLEXITY_API_KEY): TextSelector(TextSelectorConfig(type="password")),
vol.Optional(CONF_PERPLEXITY_MODEL, default=DEFAULT_MODELS["Perplexity AI"]): str,
vol.Optional(CONF_PERPLEXITY_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Range(min=0.0, max=2.0)
),
}
self._add_token_fields(schema)
return await self._provider_form(
"perplexity",
vol.Schema(schema),
_v,
"AI Automation Suggester (Perplexity)",
{},
{},
user_input,
)
async def async_step_openrouter(self, user_input=None):
async def _v(ui):
return await self.validator.validate_openrouter(
ui[CONF_OPENROUTER_API_KEY],
ui.get(CONF_OPENROUTER_MODEL, DEFAULT_MODELS["OpenRouter"]),
)
schema = {
vol.Required(CONF_OPENROUTER_API_KEY): TextSelector(TextSelectorConfig(type="password")),
vol.Optional(
CONF_OPENROUTER_MODEL, default=DEFAULT_MODELS["OpenRouter"]
): str,
vol.Optional(CONF_OPENROUTER_REASONING_MAX_TOKENS, default=0): vol.All(
vol.Coerce(int), vol.Range(min=0)
),
vol.Optional(
CONF_OPENROUTER_TEMPERATURE, default=DEFAULT_TEMPERATURE
): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
}
self._add_token_fields(schema)
return await self._provider_form(
"openrouter",
vol.Schema(schema),
_v,
"AI Automation Suggester (OpenRouter)",
{},
{},
user_input,
)
async def async_step_openai_azure(self, user_input=None):
async def _v(ui):
if not ui.get(CONF_OPENAI_AZURE_API_KEY) or not ui.get(CONF_OPENAI_AZURE_DEPLOYMENT_ID) or not ui.get(CONF_OPENAI_AZURE_API_VERSION):
return "All fields are required"
return None
schema = {
vol.Required(CONF_OPENAI_AZURE_API_KEY): TextSelector(TextSelectorConfig(type="password")),
vol.Required(CONF_OPENAI_AZURE_DEPLOYMENT_ID, default=DEFAULT_MODELS["OpenAI Azure"]): str,
vol.Required(CONF_OPENAI_AZURE_ENDPOINT, default="{your-resource-name}.openai.azure.com"): str,
vol.Required(CONF_OPENAI_AZURE_API_VERSION, default="2025-01-01-preview"): str,
vol.Optional(CONF_OPENAI_AZURE_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
}
self._add_token_fields(schema)
return await self._provider_form(
"openai_azure",
vol.Schema(schema),
_v,
"AI Automation Suggester (OpenAI Azure)",
{},
{},
user_input,
)
async def async_step_generic_openai(self, user_input=None):
"""Handle the Generic OpenAI API configuration."""
async def _v(ui):
if not ui.get(CONF_GENERIC_OPENAI_ENDPOINT):
return "API URL is required"
if ui.get(CONF_GENERIC_OPENAI_ENABLE_VALIDATION, False):
if not ui.get(CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT):
return "Validation endpoint is required when validation is enabled"
return await self.validator.validate_generic_openai(ui[CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT], ui.get(CONF_GENERIC_OPENAI_API_KEY))
schema = {
vol.Required(CONF_GENERIC_OPENAI_ENDPOINT): str,
vol.Required(CONF_GENERIC_OPENAI_API_KEY): TextSelector(TextSelectorConfig(type="password")),
vol.Required(CONF_GENERIC_OPENAI_MODEL, default=DEFAULT_MODELS["Generic OpenAI"]): str,
vol.Optional(CONF_GENERIC_OPENAI_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
vol.Optional(CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT, default=""): str,
vol.Optional(CONF_GENERIC_OPENAI_ENABLE_VALIDATION, default=False): bool,
}
self._add_token_fields(schema)
return await self._provider_form(
"generic_openai",
vol.Schema(schema),
_v,
"AI Automation Suggester (Generic OpenAI)",
{},
{},
user_input,
)
async def async_step_litellm(self, user_input=None):
"""Handle the LiteLLM configuration."""
async def _v(ui):
if not ui.get(CONF_LITELLM_MODEL):
return "Model is required (e.g. openai/gpt-4o, anthropic/claude-sonnet-4-6)"
schema = {
vol.Required(CONF_LITELLM_MODEL, default=DEFAULT_MODELS["LiteLLM"]): str,
vol.Optional(CONF_LITELLM_API_KEY, default=""): TextSelector(TextSelectorConfig(type="password")),
vol.Optional(CONF_LITELLM_API_BASE, default=""): str,
vol.Optional(CONF_LITELLM_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0)),
}
self._add_token_fields(schema)
return await self._provider_form(
"litellm",
vol.Schema(schema),
_v,
"AI Automation Suggester (LiteLLM)",
{},
{},
user_input,
)
# ───────── Options flow (edit after setup) ─────────
@staticmethod
@callback
def async_get_options_flow(config_entry):
return AIAutomationOptionsFlowHandler(config_entry)
class AIAutomationOptionsFlowHandler(config_entries.OptionsFlow):
"""Allow postsetup tweaking of models, keys, token budgets."""
def __init__(self, config_entry):
super().__init__()
self._config_entry = config_entry
def _get_option(self, key, default=None):
"""Get value from options, then data, then default."""
if key in self._config_entry.options:
return self._config_entry.options.get(key)
if key in self._config_entry.data:
return self._config_entry.data.get(key)
return default
async def async_step_init(self, user_input=None):
if user_input:
new_data = {
**self._config_entry.options,
**user_input,
CONF_MAX_INPUT_TOKENS: user_input.get(
CONF_MAX_INPUT_TOKENS,
self._get_option(CONF_MAX_INPUT_TOKENS, DEFAULT_MAX_INPUT_TOKENS)
),
CONF_MAX_OUTPUT_TOKENS: user_input.get(
CONF_MAX_OUTPUT_TOKENS,
self._get_option(CONF_MAX_OUTPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS)
),
}
return self.async_create_entry(title="", data=new_data)
provider = self._config_entry.data.get(CONF_PROVIDER)
schema: Dict[Any, Any] = {
vol.Optional(CONF_MAX_INPUT_TOKENS, default=self._get_option(CONF_MAX_INPUT_TOKENS, DEFAULT_MAX_INPUT_TOKENS)
): vol.All(vol.Coerce(int), vol.Range(min=100)),
vol.Optional(CONF_MAX_OUTPUT_TOKENS, default=self._get_option(CONF_MAX_OUTPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS)
): vol.All(vol.Coerce(int), vol.Range(min=100)),
vol.Optional(CONF_CUSTOM_SYSTEM_PROMPT, default=self._get_option(CONF_CUSTOM_SYSTEM_PROMPT, "")): str,
vol.Optional(CONF_EXCLUDED_DOMAINS, default=self._get_option(CONF_EXCLUDED_DOMAINS, "")): str,
vol.Optional(CONF_EXCLUDED_ENTITIES, default=self._get_option(CONF_EXCLUDED_ENTITIES, "")): str,
vol.Optional(CONF_EXCLUDED_AREAS, default=self._get_option(CONF_EXCLUDED_AREAS, "")): str,
vol.Optional(CONF_HISTORY_RETENTION, default=self._get_option(CONF_HISTORY_RETENTION, DEFAULT_HISTORY_RETENTION)): vol.All(vol.Coerce(int), vol.Range(min=1, max=250)),
vol.Optional(CONF_REQUEST_TIMEOUT, default=self._get_option(CONF_REQUEST_TIMEOUT, DEFAULT_REQUEST_TIMEOUT)): vol.All(vol.Coerce(int), vol.Range(min=10, max=1800)),
}
# providerspecific editable fields
if provider == "OpenAI":
schema[vol.Optional(CONF_OPENAI_API_KEY, default=self._get_option(CONF_OPENAI_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_OPENAI_MODEL, default=self._get_option(CONF_OPENAI_MODEL, DEFAULT_MODELS["OpenAI"]))] = str
schema[vol.Optional(CONF_OPENAI_TEMPERATURE, default=self._get_option(CONF_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
schema[vol.Optional(CONF_OPENAI_REASONING_EFFORT, default=self._get_option(CONF_OPENAI_REASONING_EFFORT, DEFAULT_OPENAI_REASONING_EFFORT))] = vol.In(["minimal", "low", "medium", "high"])
elif provider == "Anthropic":
schema[vol.Optional(CONF_ANTHROPIC_API_KEY, default=self._get_option(CONF_ANTHROPIC_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_ANTHROPIC_MODEL, default=self._get_option(CONF_ANTHROPIC_MODEL, DEFAULT_MODELS["Anthropic"]))] = str
schema[vol.Optional(CONF_ANTHROPIC_TEMPERATURE, default=self._get_option(CONF_ANTHROPIC_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
elif provider == "Google":
schema[vol.Optional(CONF_GOOGLE_API_KEY, default=self._get_option(CONF_GOOGLE_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_GOOGLE_MODEL, default=self._get_option(CONF_GOOGLE_MODEL, DEFAULT_MODELS["Google"]))] = str
schema[vol.Optional(CONF_GOOGLE_TEMPERATURE, default=self._get_option(CONF_GOOGLE_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
elif provider == "Groq":
schema[vol.Optional(CONF_GROQ_API_KEY, default=self._get_option(CONF_GROQ_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_GROQ_MODEL, default=self._get_option(CONF_GROQ_MODEL, DEFAULT_MODELS["Groq"]))] = str
schema[vol.Optional(CONF_GROQ_TEMPERATURE, default=self._get_option(CONF_GROQ_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
elif provider == "LocalAI":
schema[vol.Optional(CONF_LOCALAI_HTTPS, default=self._get_option(CONF_LOCALAI_HTTPS, False))] = bool
schema[vol.Optional(CONF_LOCALAI_MODEL, default=self._get_option(CONF_LOCALAI_MODEL, DEFAULT_MODELS["LocalAI"]))] = str
schema[vol.Optional(CONF_LOCALAI_TEMPERATURE, default=self._get_option(CONF_LOCALAI_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
schema[vol.Optional(CONF_LOCALAI_IP_ADDRESS, default=self._get_option(CONF_LOCALAI_IP_ADDRESS, "localhost"))] = str
schema[vol.Optional(CONF_LOCALAI_PORT, default=self._get_option(CONF_LOCALAI_PORT, 8080))] = int
elif provider == "Ollama":
schema[vol.Optional(CONF_OLLAMA_BASE_URL, default=self._get_option(CONF_OLLAMA_BASE_URL, ""))] = str
schema[vol.Optional(CONF_OLLAMA_API_KEY, default=self._get_option(CONF_OLLAMA_API_KEY, ""))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_OLLAMA_IP_ADDRESS, default=self._get_option(CONF_OLLAMA_IP_ADDRESS, "localhost"))] = str
schema[vol.Optional(CONF_OLLAMA_PORT, default=self._get_option(CONF_OLLAMA_PORT, 11434))] = int
schema[vol.Optional(CONF_OLLAMA_HTTPS, default=self._get_option(CONF_OLLAMA_HTTPS, False))] = bool
schema[vol.Optional(CONF_OLLAMA_MODEL, default=self._get_option(CONF_OLLAMA_MODEL, DEFAULT_MODELS["Ollama"]))] = str
schema[vol.Optional(CONF_OLLAMA_TEMPERATURE, default=self._get_option(CONF_OLLAMA_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
schema[vol.Optional(CONF_OLLAMA_DISABLE_THINK, default=self._get_option(CONF_OLLAMA_DISABLE_THINK, False))] = bool
elif provider == "Custom OpenAI":
schema[vol.Optional(CONF_CUSTOM_OPENAI_ENDPOINT, default=self._get_option(CONF_CUSTOM_OPENAI_ENDPOINT))] = str
schema[vol.Optional(CONF_CUSTOM_OPENAI_API_KEY, default=self._get_option(CONF_CUSTOM_OPENAI_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_CUSTOM_OPENAI_MODEL, default=self._get_option(CONF_CUSTOM_OPENAI_MODEL, DEFAULT_MODELS["Custom OpenAI"]))] = str
schema[vol.Optional(CONF_CUSTOM_OPENAI_TEMPERATURE, default=self._get_option(CONF_CUSTOM_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
elif provider == "Mistral AI":
schema[vol.Optional(CONF_MISTRAL_API_KEY, default=self._get_option(CONF_MISTRAL_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_MISTRAL_MODEL, default=self._get_option(CONF_MISTRAL_MODEL, DEFAULT_MODELS["Mistral AI"]))] = str
schema[vol.Optional(CONF_MISTRAL_TEMPERATURE, default=self._get_option(CONF_MISTRAL_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
elif provider == "Perplexity AI":
schema[vol.Optional(CONF_PERPLEXITY_API_KEY, default=self._get_option(CONF_PERPLEXITY_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_PERPLEXITY_MODEL, default=self._get_option(CONF_PERPLEXITY_MODEL, DEFAULT_MODELS["Perplexity AI"]))] = str
schema[vol.Optional(CONF_PERPLEXITY_TEMPERATURE, default=self._get_option(CONF_PERPLEXITY_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
elif provider == "OpenRouter":
schema[vol.Optional(CONF_OPENROUTER_API_KEY, default=self._get_option(CONF_OPENROUTER_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_OPENROUTER_MODEL, default=self._get_option(CONF_OPENROUTER_MODEL, DEFAULT_MODELS["OpenRouter"]))] = str
schema[vol.Optional(CONF_OPENROUTER_REASONING_MAX_TOKENS, default=self._get_option(CONF_OPENROUTER_REASONING_MAX_TOKENS, 0))] = vol.All(vol.Coerce(int), vol.Range(min=0))
schema[vol.Optional(CONF_OPENROUTER_TEMPERATURE, default=self._get_option(CONF_OPENROUTER_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
elif provider == "OpenAI Azure":
schema[vol.Optional(CONF_OPENAI_AZURE_API_KEY, default=self._get_option(CONF_OPENAI_AZURE_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_OPENAI_AZURE_ENDPOINT, default=self._get_option(CONF_OPENAI_AZURE_ENDPOINT))] = str
schema[vol.Optional(CONF_OPENAI_AZURE_DEPLOYMENT_ID, default=self._get_option(CONF_OPENAI_AZURE_DEPLOYMENT_ID, DEFAULT_MODELS["OpenAI Azure"]))] = str
schema[vol.Optional(CONF_OPENAI_AZURE_API_VERSION, default=self._get_option(CONF_OPENAI_AZURE_API_VERSION, "2025-01-01-preview"))] = str
schema[vol.Optional(CONF_OPENAI_AZURE_TEMPERATURE, default=self._get_option(CONF_OPENAI_AZURE_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
elif provider == "Generic OpenAI":
schema[vol.Optional(CONF_GENERIC_OPENAI_API_KEY, default=self._get_option(CONF_GENERIC_OPENAI_API_KEY))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_GENERIC_OPENAI_ENDPOINT, default=self._get_option(CONF_GENERIC_OPENAI_ENDPOINT))] = str
schema[vol.Optional(CONF_GENERIC_OPENAI_MODEL, default=self._get_option(CONF_GENERIC_OPENAI_MODEL, DEFAULT_MODELS["Generic OpenAI"]))] = str
schema[vol.Optional(CONF_GENERIC_OPENAI_TEMPERATURE, default=self._get_option(CONF_GENERIC_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
schema[vol.Optional(CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT, default=self._get_option(CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT, ""))] = str
schema[vol.Optional(CONF_GENERIC_OPENAI_ENABLE_VALIDATION, default=self._get_option(CONF_GENERIC_OPENAI_ENABLE_VALIDATION, False))] = bool
elif provider == "LiteLLM":
schema[vol.Optional(CONF_LITELLM_API_KEY, default=self._get_option(CONF_LITELLM_API_KEY, ""))] = TextSelector(TextSelectorConfig(type="password"))
schema[vol.Optional(CONF_LITELLM_MODEL, default=self._get_option(CONF_LITELLM_MODEL, DEFAULT_MODELS["LiteLLM"]))] = str
schema[vol.Optional(CONF_LITELLM_API_BASE, default=self._get_option(CONF_LITELLM_API_BASE, ""))] = str
schema[vol.Optional(CONF_LITELLM_TEMPERATURE, default=self._get_option(CONF_LITELLM_TEMPERATURE, DEFAULT_TEMPERATURE))] = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=2.0))
return self.async_show_form(step_id="init", data_schema=vol.Schema(schema))
@@ -0,0 +1,193 @@
"""Constants for the AI Automation Suggester integration."""
# ─────────────────────────────────────────────────────────────
# Core
# ─────────────────────────────────────────────────────────────
DOMAIN = "ai_automation_suggester"
PLATFORMS = ["sensor"]
CONFIG_VERSION = 3 # config-entry version (used by async_migrate_entry)
INTEGRATION_NAME = "AI Automation Suggester"
# ─────────────────────────────────────────────────────────────
# Token budgeting
# ─────────────────────────────────────────────────────────────
# Single legacy knob (kept for backward compatibility)
CONF_MAX_TOKENS = "max_tokens"
DEFAULT_MAX_TOKENS = 500 # legacy default used for both budgets if new keys absent
# New, separate knobs (Issue #91)
CONF_MAX_INPUT_TOKENS = "max_input_tokens" # how much of the prompt we keep
CONF_MAX_OUTPUT_TOKENS = "max_output_tokens" # how long the AI response may be
DEFAULT_MAX_INPUT_TOKENS = DEFAULT_MAX_TOKENS
DEFAULT_MAX_OUTPUT_TOKENS = DEFAULT_MAX_TOKENS
DEFAULT_TEMPERATURE = 0.7
DEFAULT_REQUEST_TIMEOUT = 900
DEFAULT_HISTORY_RETENTION = 25
DEFAULT_OPENAI_REASONING_EFFORT = "low"
# ─────────────────────────────────────────────────────────────
# Providerselection key
# ─────────────────────────────────────────────────────────────
CONF_PROVIDER = "provider"
CONF_CUSTOM_SYSTEM_PROMPT = "custom_system_prompt"
CONF_EXCLUDED_DOMAINS = "excluded_domains"
CONF_EXCLUDED_ENTITIES = "excluded_entities"
CONF_EXCLUDED_AREAS = "excluded_areas"
CONF_HISTORY_RETENTION = "history_retention"
CONF_REQUEST_TIMEOUT = "request_timeout"
# ─────────────────────────────────────────────────────────────
# Providerspecific keys
# ─────────────────────────────────────────────────────────────
# OpenAI
CONF_OPENAI_API_KEY = "openai_api_key"
CONF_OPENAI_MODEL = "openai_model"
CONF_OPENAI_TEMPERATURE = "openai_temperature"
CONF_OPENAI_REASONING_EFFORT = "openai_reasoning_effort"
# OpenAI Azure
CONF_OPENAI_AZURE_API_KEY = "openai_azure_api_key"
CONF_OPENAI_AZURE_DEPLOYMENT_ID = "openai_azure_deployment_id"
CONF_OPENAI_AZURE_API_VERSION = "openai_azure_api_version"
CONF_OPENAI_AZURE_ENDPOINT = "openai_azure_endpoint"
CONF_OPENAI_AZURE_TEMPERATURE = "openai_azure_temperature"
# Anthropic
CONF_ANTHROPIC_API_KEY = "anthropic_api_key"
CONF_ANTHROPIC_MODEL = "anthropic_model"
CONF_ANTHROPIC_TEMPERATURE = "anthropic_temperature"
VERSION_ANTHROPIC = "2023-06-01"
# Google
CONF_GOOGLE_API_KEY = "google_api_key"
CONF_GOOGLE_MODEL = "google_model"
CONF_GOOGLE_TEMPERATURE = "google_temperature"
# Groq
CONF_GROQ_API_KEY = "groq_api_key"
CONF_GROQ_MODEL = "groq_model"
CONF_GROQ_TEMPERATURE = "groq_temperature"
# LocalAI
CONF_LOCALAI_IP_ADDRESS = "localai_ip"
CONF_LOCALAI_PORT = "localai_port"
CONF_LOCALAI_HTTPS = "localai_https"
CONF_LOCALAI_MODEL = "localai_model"
CONF_LOCALAI_TEMPERATURE = "localai_temperature"
# Ollama
CONF_OLLAMA_IP_ADDRESS = "ollama_ip"
CONF_OLLAMA_PORT = "ollama_port"
CONF_OLLAMA_HTTPS = "ollama_https"
CONF_OLLAMA_BASE_URL = "ollama_base_url"
CONF_OLLAMA_API_KEY = "ollama_api_key"
CONF_OLLAMA_MODEL = "ollama_model"
CONF_OLLAMA_TEMPERATURE = "ollama_temperature"
CONF_OLLAMA_DISABLE_THINK = "ollama_disable_think"
# Custom OpenAI
CONF_CUSTOM_OPENAI_ENDPOINT = "custom_openai_endpoint"
CONF_CUSTOM_OPENAI_API_KEY = "custom_openai_api_key"
CONF_CUSTOM_OPENAI_MODEL = "custom_openai_model"
CONF_CUSTOM_OPENAI_TEMPERATURE = "custom_openai_temperature"
# Mistral AI
CONF_MISTRAL_API_KEY = "mistral_api_key"
CONF_MISTRAL_MODEL = "mistral_model"
CONF_MISTRAL_TEMPERATURE = "mistral_temperature"
MISTRAL_MODELS = [
"mistral-small-latest",
"mistral-medium-latest",
"mistral-large-latest",
"ministral-8b-latest",
"codestral-latest",
]
# Perplexity AI
CONF_PERPLEXITY_API_KEY = "perplexity_api_key"
CONF_PERPLEXITY_MODEL = "perplexity_model"
CONF_PERPLEXITY_TEMPERATURE = "perplexity_temperature"
# OpenRouter
CONF_OPENROUTER_API_KEY = "openrouter_api_key"
CONF_OPENROUTER_MODEL = "openrouter_model"
CONF_OPENROUTER_REASONING_MAX_TOKENS = "openrouter_reasoning_max_tokens"
CONF_OPENROUTER_TEMPERATURE = "openrouter_temperature"
# Generic OpenAI
CONF_GENERIC_OPENAI_ENDPOINT = "generic_openai_api_endpoint"
CONF_GENERIC_OPENAI_API_KEY = "generic_openai_api_key"
CONF_GENERIC_OPENAI_MODEL = "generic_openai_model"
CONF_GENERIC_OPENAI_TEMPERATURE = "generic_openai_temperature"
CONF_GENERIC_OPENAI_VALIDATION_ENDPOINT = "generic_openai_validation_endpoint"
CONF_GENERIC_OPENAI_ENABLE_VALIDATION = "generic_openai_enable_validation"
# LiteLLM
CONF_LITELLM_API_KEY = "litellm_api_key"
CONF_LITELLM_MODEL = "litellm_model"
CONF_LITELLM_TEMPERATURE = "litellm_temperature"
CONF_LITELLM_API_BASE = "litellm_api_base"
# ─────────────────────────────────────────────────────────────
# Model defaults per provider
# ─────────────────────────────────────────────────────────────
DEFAULT_MODELS = {
"OpenAI": "gpt-5.4-mini",
"OpenAI Azure": "gpt-5.4-mini",
"Anthropic": "claude-sonnet-4-6",
"Google": "gemini-2.5-flash",
"Groq": "llama-3.3-70b-versatile",
"LocalAI": "llama3",
"Ollama": "llama3.1",
"Custom OpenAI": "gpt-4o-mini",
"Mistral AI": "mistral-small-latest",
"Perplexity AI": "sonar",
"OpenRouter": "openai/gpt-5.4-mini",
"Generic OpenAI": "gpt-4o-mini",
"LiteLLM": "openai/gpt-4o-mini",
}
# ─────────────────────────────────────────────────────────────
# Service & attribute names
# ─────────────────────────────────────────────────────────────
ATTR_PROVIDER_CONFIG = "provider_config"
ATTR_CUSTOM_PROMPT = "custom_prompt"
SERVICE_GENERATE_SUGGESTIONS = "generate_suggestions"
SERVICE_CLEAR_HISTORY = "clear_history"
SERVICE_UPDATE_SUGGESTION = "update_suggestion"
# ─────────────────────────────────────────────────────────────
# Providerstatus sensor values
# ─────────────────────────────────────────────────────────────
PROVIDER_STATUS_CONNECTED = "connected"
PROVIDER_STATUS_DISCONNECTED = "disconnected"
PROVIDER_STATUS_ERROR = "error"
PROVIDER_STATUS_INITIALIZING = "initializing"
# ─────────────────────────────────────────────────────────────
# REST endpoints
# ─────────────────────────────────────────────────────────────
ENDPOINT_OPENAI = "https://api.openai.com/v1/chat/completions"
ENDPOINT_OPENAI_AZURE = "https://{endpoint}/openai/deployments/{deployment-id}/chat/completions?api-version={api_version}"
ENDPOINT_ANTHROPIC = "https://api.anthropic.com/v1/messages"
ENDPOINT_GOOGLE = "https://generativelanguage.googleapis.com/v1beta2/models/{model}:generateText?key={api_key}"
ENDPOINT_GROQ = "https://api.groq.com/openai/v1/chat/completions"
ENDPOINT_LOCALAI = "{protocol}://{ip_address}:{port}/v1/chat/completions"
ENDPOINT_OLLAMA = "{protocol}://{ip_address}:{port}/api/chat"
ENDPOINT_MISTRAL = "https://api.mistral.ai/v1/chat/completions"
ENDPOINT_PERPLEXITY = "https://api.perplexity.ai/chat/completions"
ENDPOINT_OPENROUTER = "https://openrouter.ai/api/v1/chat/completions"
# ─────────────────────────────────────────────────────────────
# Sensor Keys
# ─────────────────────────────────────────────────────────────
SENSOR_KEY_SUGGESTIONS = "suggestions"
SENSOR_KEY_STATUS = "status"
SENSOR_KEY_INPUT_TOKENS = "input_tokens"
SENSOR_KEY_OUTPUT_TOKENS = "output_tokens"
SENSOR_KEY_MODEL = "model"
SENSOR_KEY_LAST_ERROR = "last_error"
SENSOR_KEY_HISTORY_COUNT = "history_count"
@@ -0,0 +1,920 @@
"""Coordinator for AI Automation Suggester."""
from __future__ import annotations
import asyncio
from datetime import datetime
import logging
from pathlib import Path
import random
import re
from typing import Any
import aiohttp
import anyio
import yaml
from homeassistant.components import persistent_notification
from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import *
from .endpoint_utils import bearer_auth_headers, ollama_api_candidates, ollama_base_url, openai_chat_endpoint
from .language_utils import suggestion_language_instruction
from .model_catalog import (
chat_token_parameter,
compatibility_warnings,
google_json_schema_response_format,
json_schema_response_format,
model_uses_responses_api,
should_send_temperature,
supports_json_schema,
)
from .store import async_get_suggestion_store
from .suggestions import (
STRUCTURED_OUTPUT_INSTRUCTIONS,
format_suggestion_notification,
parse_suggestion_response,
)
_LOGGER = logging.getLogger(__name__)
SYSTEM_PROMPT = """You are an AI assistant that generates Home Assistant automations
based on entities, areas and devices, and suggests improvements to existing automations.
For each entity:
1. Understand its function and context.
2. Consider its current state and attributes.
3. Suggest context-aware automations or tweaks, including real entity_ids.
If asked to focus on a theme (energy saving, presence lighting, etc.), integrate it.
Also review existing automations and propose improvements.
If you see a lot of text in a different language, focus on it for a translation for your output.
"""
class AIAutomationCoordinator(DataUpdateCoordinator):
"""Build prompts, call the configured provider, and publish suggestions."""
def __init__(self, hass: HomeAssistant, entry) -> None:
self.hass = hass
self.entry = entry
self.previous_entities: dict[str, dict] = {}
self.last_update: datetime | None = None
self.session = async_get_clientsession(hass)
self._generation_lock = asyncio.Lock()
self._last_error: str | None = None
self._last_response_metadata: dict[str, Any] = {}
self.SYSTEM_PROMPT = SYSTEM_PROMPT
self.scan_all = False
self.selected_domains: list[str] = []
self.excluded_domains: list[str] = self._opt_list(CONF_EXCLUDED_DOMAINS)
self.excluded_entities: list[str] = self._opt_list(CONF_EXCLUDED_ENTITIES)
self.excluded_areas: list[str] = self._opt_list(CONF_EXCLUDED_AREAS)
self.entity_limit = 200
self.automation_read_file = False
self.automation_limit = 100
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=None)
self.data: dict = {
"suggestions": "No suggestions yet",
"suggestion": None,
"suggestion_history": [],
"suggestion_count": 0,
"description": None,
"yaml_block": None,
"last_update": None,
"entities_processed": [],
"provider": self._opt(CONF_PROVIDER, "unknown"),
"model": self._current_model(),
"warnings": [],
"last_error": None,
"response_metadata": {},
}
self.device_registry: dr.DeviceRegistry | None = None
self.entity_registry: er.EntityRegistry | None = None
self.area_registry: ar.AreaRegistry | None = None
def _opt(self, key: str, default=None):
"""Return entry option, then setup data, then default."""
return self.entry.options.get(key, self.entry.data.get(key, default))
def _opt_list(self, key: str, default: list[str] | None = None) -> list[str]:
"""Return a normalized list option."""
value = self._opt(key, default or [])
return self._normalize_list(value)
@staticmethod
def _normalize_list(value: Any) -> list[str]:
"""Normalize strings, dicts, tuples, and lists into a string list."""
if value is None:
return []
if isinstance(value, str):
return [item.strip() for item in value.split(",") if item.strip()]
if isinstance(value, dict):
return [str(item).strip() for item in value if str(item).strip()]
if isinstance(value, (list, tuple, set)):
return [str(item).strip() for item in value if str(item).strip()]
return [str(value).strip()] if str(value).strip() else []
def _budgets(self) -> tuple[int, int]:
"""Return input and output token budgets with legacy fallback."""
out_budget = self._opt(
CONF_MAX_OUTPUT_TOKENS, self._opt(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
)
in_budget = self._opt(
CONF_MAX_INPUT_TOKENS, self._opt(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
)
return int(in_budget), int(out_budget)
def _timeout(self) -> aiohttp.ClientTimeout:
seconds = int(self._opt(CONF_REQUEST_TIMEOUT, DEFAULT_REQUEST_TIMEOUT))
return aiohttp.ClientTimeout(total=max(10, seconds))
def _current_model(self, provider: str | None = None) -> str:
provider = provider or self._opt(CONF_PROVIDER, "OpenAI")
model_key_map = {
"OpenAI": CONF_OPENAI_MODEL,
"Anthropic": CONF_ANTHROPIC_MODEL,
"Google": CONF_GOOGLE_MODEL,
"Groq": CONF_GROQ_MODEL,
"LocalAI": CONF_LOCALAI_MODEL,
"Ollama": CONF_OLLAMA_MODEL,
"Custom OpenAI": CONF_CUSTOM_OPENAI_MODEL,
"Mistral AI": CONF_MISTRAL_MODEL,
"Perplexity AI": CONF_PERPLEXITY_MODEL,
"OpenRouter": CONF_OPENROUTER_MODEL,
"OpenAI Azure": CONF_OPENAI_AZURE_DEPLOYMENT_ID,
"Generic OpenAI": CONF_GENERIC_OPENAI_MODEL,
"LiteLLM": CONF_LITELLM_MODEL,
}
model_key = model_key_map.get(provider)
return self._opt(model_key, DEFAULT_MODELS.get(provider, "unknown")) if model_key else "unknown"
async def async_added_to_hass(self):
await super().async_added_to_hass()
self.device_registry = dr.async_get(self.hass)
self.entity_registry = er.async_get(self.hass)
self.area_registry = ar.async_get(self.hass)
async def async_shutdown(self):
return
async def async_generate_suggestions(
self,
*,
custom_prompt: str | None = None,
all_entities: bool = False,
domains: Any = None,
exclude_domains: Any = None,
exclude_entities: Any = None,
exclude_areas: Any = None,
entity_limit: int = 200,
automation_read_yaml: bool = False,
automation_limit: int = 100,
) -> None:
"""Run one suggestion generation with isolated request settings."""
async with self._generation_lock:
saved = {
"SYSTEM_PROMPT": self.SYSTEM_PROMPT,
"scan_all": self.scan_all,
"selected_domains": self.selected_domains,
"excluded_domains": self.excluded_domains,
"excluded_entities": self.excluded_entities,
"excluded_areas": self.excluded_areas,
"entity_limit": self.entity_limit,
"automation_read_file": self.automation_read_file,
"automation_limit": self.automation_limit,
}
try:
persistent_prompt = str(self._opt(CONF_CUSTOM_SYSTEM_PROMPT, "") or "").strip()
prompt_parts = [SYSTEM_PROMPT]
if persistent_prompt:
prompt_parts.append(f"Persistent user instructions:\n{persistent_prompt}")
if custom_prompt:
prompt_parts.append(f"Request-specific instructions:\n{custom_prompt}")
self.SYSTEM_PROMPT = "\n\n".join(prompt_parts)
self.scan_all = all_entities
self.selected_domains = self._normalize_list(domains)
self.excluded_domains = self._normalize_list(exclude_domains) or self._opt_list(CONF_EXCLUDED_DOMAINS)
self.excluded_entities = self._normalize_list(exclude_entities) or self._opt_list(CONF_EXCLUDED_ENTITIES)
self.excluded_areas = self._normalize_list(exclude_areas) or self._opt_list(CONF_EXCLUDED_AREAS)
self.entity_limit = int(entity_limit)
self.automation_read_file = bool(automation_read_yaml)
self.automation_limit = int(automation_limit)
await self.async_request_refresh()
finally:
self.SYSTEM_PROMPT = saved["SYSTEM_PROMPT"]
self.scan_all = saved["scan_all"]
self.selected_domains = saved["selected_domains"]
self.excluded_domains = saved["excluded_domains"]
self.excluded_entities = saved["excluded_entities"]
self.excluded_areas = saved["excluded_areas"]
self.entity_limit = saved["entity_limit"]
self.automation_read_file = saved["automation_read_file"]
self.automation_limit = saved["automation_limit"]
async def _async_update_data(self) -> dict:
try:
now = datetime.now()
provider = self._opt(CONF_PROVIDER, "OpenAI")
model = self._current_model(provider)
warnings = compatibility_warnings(provider, model)
self.last_update = now
self._last_error = None
self._last_response_metadata = {}
current = self._collect_entities()
picked = current if self.scan_all else {k: v for k, v in current.items() if k not in self.previous_entities}
if not picked:
self.previous_entities = current
history = await async_get_suggestion_store(self.hass).async_list()
self.data.update(
{
"suggestion_history": history,
"suggestion_count": len(history),
"provider": provider,
"model": model,
"warnings": warnings,
"last_update": now,
}
)
return self.data
prompt = await self._build_prompt(picked)
response = await self._dispatch(prompt)
store = async_get_suggestion_store(self.hass)
if response:
parsed = parse_suggestion_response(
response,
provider=provider,
model=model,
created_at=now,
entities_processed=list(picked.keys()),
inherited_warnings=warnings,
response_metadata=self._last_response_metadata,
)
retention = int(self._opt(CONF_HISTORY_RETENTION, DEFAULT_HISTORY_RETENTION))
history = await store.async_add_suggestions(parsed, retention=retention)
latest = history[0] if history else parsed[0]
persistent_notification.async_create(
self.hass,
message=format_suggestion_notification(latest),
title=f"AI Automation Suggestions ({provider})",
notification_id=f"ai_automation_suggestions_{now.timestamp()}",
)
self.data = {
"suggestions": response,
"suggestion": latest,
"suggestion_history": history,
"suggestion_count": len(history),
"description": latest.get("description"),
"yaml_block": latest.get("yamlCode"),
"last_update": now,
"entities_processed": list(picked.keys()),
"provider": provider,
"model": model,
"warnings": latest.get("warnings", warnings),
"last_error": None,
"response_metadata": self._last_response_metadata,
}
else:
history = await store.async_list()
self.data.update(
{
"suggestions": "No suggestions available",
"suggestion": None,
"suggestion_history": history,
"suggestion_count": len(history),
"description": None,
"yaml_block": None,
"last_update": now,
"entities_processed": [],
"provider": provider,
"model": model,
"warnings": warnings,
"last_error": self._last_error,
"response_metadata": self._last_response_metadata,
}
)
self.previous_entities = current
return self.data
except Exception as err: # noqa: BLE001
self._last_error = str(err)
_LOGGER.exception("Coordinator fatal error")
self.data["last_error"] = self._last_error
return self.data
def _collect_entities(self) -> dict[str, dict]:
current: dict[str, dict] = {}
selected_domains = set(self.selected_domains)
for entity_id in self.hass.states.async_entity_ids():
domain = entity_id.split(".", 1)[0]
if selected_domains and domain not in selected_domains:
continue
if self._is_entity_excluded(entity_id):
continue
state = self.hass.states.get(entity_id)
if state:
current[entity_id] = {
"state": state.state,
"attributes": state.attributes,
"last_changed": state.last_changed,
"last_updated": state.last_updated,
"friendly_name": state.attributes.get("friendly_name", entity_id),
}
return current
def _is_entity_excluded(self, entity_id: str) -> bool:
domain = entity_id.split(".", 1)[0]
if domain in set(self.excluded_domains):
return True
if entity_id in set(self.excluded_entities):
return True
if not self.excluded_areas or not self.entity_registry:
return False
entity_entry = self.entity_registry.async_get(entity_id)
device_entry = None
if entity_entry and entity_entry.device_id and self.device_registry:
device_entry = self.device_registry.async_get(entity_entry.device_id)
area_id = entity_entry.area_id if entity_entry and entity_entry.area_id else None
if not area_id and device_entry:
area_id = device_entry.area_id
area_names = {area_id.lower()} if area_id else set()
if area_id and self.area_registry:
area_entry = self.area_registry.async_get_area(area_id)
if area_entry:
area_names.add(area_entry.name.lower())
excluded = {area.lower() for area in self.excluded_areas}
return bool(area_names & excluded)
async def _build_prompt(self, entities: dict) -> str:
max_attr = 500
max_autom = self.automation_limit
ent_sections: list[str] = []
for entity_id, meta in random.sample(list(entities.items()), min(len(entities), self.entity_limit)):
domain = entity_id.split(".", 1)[0]
attr_str = str(meta["attributes"])
if len(attr_str) > max_attr:
attr_str = f"{attr_str[:max_attr]}...(truncated)"
entity_entry = self.entity_registry.async_get(entity_id) if self.entity_registry else None
device_entry = (
self.device_registry.async_get(entity_entry.device_id)
if entity_entry and entity_entry.device_id and self.device_registry
else None
)
area_id = entity_entry.area_id if entity_entry and entity_entry.area_id else None
if not area_id and device_entry:
area_id = device_entry.area_id
area_name = "Unknown Area"
if area_id and self.area_registry:
area_entry = self.area_registry.async_get_area(area_id)
if area_entry:
area_name = area_entry.name
block = (
f"Entity: {entity_id}\n"
f"Friendly Name: {meta['friendly_name']}\n"
f"Domain: {domain}\n"
f"State: {meta['state']}\n"
f"Attributes: {attr_str}\n"
f"Area: {area_name}\n"
)
if device_entry:
block += (
"Device Info:\n"
f" Manufacturer: {device_entry.manufacturer}\n"
f" Model: {device_entry.model}\n"
f" Device Name: {device_entry.name_by_user or device_entry.name}\n"
f" Device ID: {device_entry.id}\n"
)
block += f"Last Changed: {meta['last_changed']}\nLast Updated: {meta['last_updated']}\n---\n"
ent_sections.append(block)
autom_sections = self._read_automations_default(max_autom, max_attr)
autom_codes: list[str] = []
if self.automation_read_file:
autom_codes = await self._read_automations_file_method(max_autom)
language_instruction = suggestion_language_instruction(getattr(self.hass.config, "language", None))
language_block = f"{language_instruction}\n\n" if language_instruction else ""
return (
f"{self.SYSTEM_PROMPT}\n\n"
f"{STRUCTURED_OUTPUT_INSTRUCTIONS}\n\n"
f"{language_block}"
f"Entities in your Home Assistant (sampled):\n{''.join(ent_sections)}\n"
"Existing Automations Overview:\n"
f"{''.join(autom_sections) if autom_sections else 'None found.'}\n\n"
"Automations YAML Code (for analysis and improvement):\n"
f"{''.join(autom_codes) if autom_codes else 'No automations YAML code included.'}\n\n"
"Analyze the entities and existing automations. Propose useful new automations or improvements "
"that reference only the entity_ids shown above."
)
def _read_automations_default(self, max_autom: int, max_attr: int) -> list[str]:
autom_sections: list[str] = []
for automation_id in self.hass.states.async_entity_ids("automation")[:max_autom]:
state = self.hass.states.get(automation_id)
if state:
attr = str(state.attributes)
if len(attr) > max_attr:
attr = f"{attr[:max_attr]}...(truncated)"
autom_sections.append(
f"Entity: {automation_id}\n"
f"Friendly Name: {state.attributes.get('friendly_name', automation_id)}\n"
f"State: {state.state}\n"
f"Attributes: {attr}\n"
"---\n"
)
return autom_sections
async def _read_automations_file_method(self, max_autom: int) -> list[str]:
automations_file = Path(self.hass.config.path()) / "automations.yaml"
autom_codes: list[str] = []
try:
async with await anyio.open_file(automations_file, "r", encoding="utf-8") as file:
content = await file.read()
automations = yaml.safe_load(content) or []
if not isinstance(automations, list):
_LOGGER.warning("automations.yaml did not parse as a list")
return autom_codes
for automation in automations[:max_autom]:
if not isinstance(automation, dict):
continue
autom_codes.append(
"Automation YAML:\n```yaml\n"
f"{yaml.safe_dump([automation], sort_keys=False)}"
"```\n---\n"
)
except FileNotFoundError:
_LOGGER.warning("automations.yaml file was not found")
except yaml.YAMLError as err:
_LOGGER.warning("Error parsing automations.yaml: %s", err)
return autom_codes
async def _dispatch(self, prompt: str) -> str | None:
provider = self._opt(CONF_PROVIDER, "OpenAI")
dispatch = {
"OpenAI": self._openai,
"Anthropic": self._anthropic,
"Google": self._google,
"Groq": self._groq,
"LocalAI": self._localai,
"Ollama": self._ollama,
"Custom OpenAI": self._custom_openai,
"Mistral AI": self._mistral,
"Perplexity AI": self._perplexity,
"OpenRouter": self._openrouter,
"OpenAI Azure": self._openai_azure,
"Generic OpenAI": self._generic_openai,
"LiteLLM": self._litellm,
}
handler = dispatch.get(provider)
if handler is None:
self._last_error = f"Unknown provider '{provider}'"
_LOGGER.error(self._last_error)
return None
try:
return await handler(prompt)
except Exception as err: # noqa: BLE001
self._last_error = str(err)
_LOGGER.exception("Dispatch error for %s", provider)
return None
def _trim_prompt(self, prompt: str) -> str:
in_budget, _ = self._budgets()
if len(prompt) // 4 > in_budget:
return prompt[: in_budget * 4]
return prompt
async def _post_json(
self,
endpoint: str,
*,
headers: dict[str, str] | None = None,
body: dict[str, Any],
provider_label: str,
) -> dict[str, Any] | None:
async with self.session.post(
endpoint,
headers=headers,
json=body,
timeout=self._timeout(),
) as response:
response_text = await response.text()
if response.status != 200:
self._last_error = f"{provider_label} error {response.status}: {response_text}"
_LOGGER.error(self._last_error)
return None
try:
return await response.json(content_type=None)
except Exception as err: # noqa: BLE001
self._last_error = f"{provider_label} returned non-JSON response: {err}: {response_text[:500]}"
_LOGGER.error(self._last_error)
return None
def _openai_compatible_body(
self,
*,
provider: str,
model: str,
prompt: str,
temperature: float,
extra: dict[str, Any] | None = None,
) -> dict[str, Any]:
_, out_budget = self._budgets()
body: dict[str, Any] = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
chat_token_parameter(provider, model): out_budget,
}
if should_send_temperature(provider, model):
body["temperature"] = temperature
if supports_json_schema(provider, model):
body["response_format"] = json_schema_response_format()
if extra:
body.update(extra)
return body
def _extract_chat_content(self, response: dict[str, Any], provider_label: str) -> str | None:
choices = response.get("choices")
if not isinstance(choices, list) or not choices:
raise ValueError(f"{provider_label} response missing choices array: {response}")
choice = choices[0]
self._last_response_metadata = {
"finish_reason": choice.get("finish_reason"),
"native_finish_reason": choice.get("native_finish_reason"),
"usage": response.get("usage"),
}
message = choice.get("message") or {}
content = message.get("content")
if isinstance(content, str):
return content
if isinstance(content, list):
return "".join(part.get("text", "") for part in content if isinstance(part, dict))
raise ValueError(f"{provider_label} message missing content: {message}")
async def _openai(self, prompt: str) -> str | None:
api_key = self._opt(CONF_OPENAI_API_KEY)
if not api_key:
raise ValueError("OpenAI API key not configured")
model = self._current_model("OpenAI")
prompt = self._trim_prompt(prompt)
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
if model_uses_responses_api("OpenAI", model):
return await self._openai_responses(prompt, model, headers)
body = self._openai_compatible_body(
provider="OpenAI",
model=model,
prompt=prompt,
temperature=float(self._opt(CONF_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE)),
)
response = await self._post_json(ENDPOINT_OPENAI, headers=headers, body=body, provider_label="OpenAI")
return self._extract_chat_content(response, "OpenAI") if response else None
async def _openai_responses(self, prompt: str, model: str, headers: dict[str, str]) -> str | None:
_, out_budget = self._budgets()
body: dict[str, Any] = {
"model": model,
"input": [{"role": "user", "content": prompt}],
"max_output_tokens": out_budget,
"reasoning": {
"effort": self._opt(CONF_OPENAI_REASONING_EFFORT, DEFAULT_OPENAI_REASONING_EFFORT)
},
"text": {"format": {"type": "json_schema", **json_schema_response_format()["json_schema"]}},
}
response = await self._post_json(
"https://api.openai.com/v1/responses",
headers=headers,
body=body,
provider_label="OpenAI Responses",
)
if not response:
return None
self._last_response_metadata = {
"status": response.get("status"),
"incomplete_details": response.get("incomplete_details"),
"usage": response.get("usage"),
}
if isinstance(response.get("output_text"), str):
return response["output_text"]
output = response.get("output") or []
text_parts: list[str] = []
for item in output:
for content in item.get("content", []) if isinstance(item, dict) else []:
if isinstance(content, dict) and content.get("type") in {"output_text", "text"}:
text_parts.append(str(content.get("text", "")))
return "".join(text_parts) if text_parts else None
async def _openai_azure(self, prompt: str) -> str | None:
endpoint_base = self._opt(CONF_OPENAI_AZURE_ENDPOINT)
api_key = self._opt(CONF_OPENAI_AZURE_API_KEY)
deployment_id = self._opt(CONF_OPENAI_AZURE_DEPLOYMENT_ID)
api_version = self._opt(CONF_OPENAI_AZURE_API_VERSION, "2025-01-01-preview")
if not endpoint_base or not deployment_id or not api_key:
raise ValueError("Azure OpenAI endpoint, deployment, or API key not configured")
endpoint_base = str(endpoint_base).rstrip("/")
if not re.match(r"^https?://", endpoint_base):
endpoint_base = f"https://{endpoint_base}"
endpoint = f"{endpoint_base}/openai/deployments/{deployment_id}/chat/completions?api-version={api_version}"
_, out_budget = self._budgets()
model = self._current_model("OpenAI Azure")
body: dict[str, Any] = {
"messages": [{"role": "user", "content": self._trim_prompt(prompt)}],
chat_token_parameter("OpenAI Azure", model): out_budget,
}
if should_send_temperature("OpenAI Azure", model):
body["temperature"] = float(self._opt(CONF_OPENAI_AZURE_TEMPERATURE, DEFAULT_TEMPERATURE))
response = await self._post_json(
endpoint,
headers={"api-key": api_key, "Content-Type": "application/json"},
body=body,
provider_label="Azure OpenAI",
)
return self._extract_chat_content(response, "Azure OpenAI") if response else None
async def _generic_openai(self, prompt: str) -> str | None:
endpoint = str(self._opt(CONF_GENERIC_OPENAI_ENDPOINT) or "").rstrip("/")
if not endpoint or not re.match(r"^https?://", endpoint):
raise ValueError("Generic OpenAI endpoint must be a full http(s) URL")
api_key = self._opt(CONF_GENERIC_OPENAI_API_KEY)
model = self._current_model("Generic OpenAI")
headers = {"Content-Type": "application/json"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
body = self._openai_compatible_body(
provider="Generic OpenAI",
model=model,
prompt=self._trim_prompt(prompt),
temperature=float(self._opt(CONF_GENERIC_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE)),
)
response = await self._post_json(endpoint, headers=headers, body=body, provider_label="Generic OpenAI")
return self._extract_chat_content(response, "Generic OpenAI") if response else None
async def _anthropic(self, prompt: str) -> str | None:
api_key = self._opt(CONF_ANTHROPIC_API_KEY)
if not api_key:
raise ValueError("Anthropic API key not configured")
_, out_budget = self._budgets()
model = self._current_model("Anthropic")
body: dict[str, Any] = {
"model": model,
"messages": [{"role": "user", "content": [{"type": "text", "text": self._trim_prompt(prompt)}]}],
"max_tokens": out_budget,
"temperature": float(self._opt(CONF_ANTHROPIC_TEMPERATURE, DEFAULT_TEMPERATURE)),
}
response = await self._post_json(
ENDPOINT_ANTHROPIC,
headers={"x-api-key": api_key, "Content-Type": "application/json", "anthropic-version": VERSION_ANTHROPIC},
body=body,
provider_label="Anthropic",
)
if not response:
return None
self._last_response_metadata = {
"stop_reason": response.get("stop_reason"),
"usage": response.get("usage"),
}
content = response.get("content") or []
text_parts = [part.get("text", "") for part in content if isinstance(part, dict) and part.get("type") == "text"]
if text_parts:
return "".join(text_parts)
raise ValueError(f"Anthropic response missing text content: {response}")
async def _google(self, prompt: str) -> str | None:
api_key = self._opt(CONF_GOOGLE_API_KEY)
if not api_key:
raise ValueError("Google API key not configured")
_, out_budget = self._budgets()
model = self._current_model("Google")
generation_config: dict[str, Any] = {
"temperature": float(self._opt(CONF_GOOGLE_TEMPERATURE, DEFAULT_TEMPERATURE)),
"maxOutputTokens": out_budget,
}
if supports_json_schema("Google", model):
generation_config["responseMimeType"] = "application/json"
generation_config["responseSchema"] = google_json_schema_response_format()["json_schema"]["schema"]
body = {"contents": [{"parts": [{"text": self._trim_prompt(prompt)}]}], "generationConfig": generation_config}
endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
response = await self._post_json(endpoint, body=body, provider_label="Google")
if not response:
return None
candidates = response.get("candidates") or []
if not candidates:
raise ValueError(f"Google response missing candidates: {response}")
self._last_response_metadata = {
"finish_reason": candidates[0].get("finishReason"),
"usage": response.get("usageMetadata"),
}
parts = candidates[0].get("content", {}).get("parts", [])
text_parts = [part.get("text", "") for part in parts if isinstance(part, dict)]
return "".join(text_parts) if text_parts else None
async def _groq(self, prompt: str) -> str | None:
api_key = self._opt(CONF_GROQ_API_KEY)
if not api_key:
raise ValueError("Groq API key not configured")
model = self._current_model("Groq")
body = self._openai_compatible_body(
provider="Groq",
model=model,
prompt=self._trim_prompt(prompt),
temperature=float(self._opt(CONF_GROQ_TEMPERATURE, DEFAULT_TEMPERATURE)),
)
response = await self._post_json(
ENDPOINT_GROQ,
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
body=body,
provider_label="Groq",
)
return self._extract_chat_content(response, "Groq") if response else None
async def _localai(self, prompt: str) -> str | None:
ip = self._opt(CONF_LOCALAI_IP_ADDRESS)
port = self._opt(CONF_LOCALAI_PORT)
if not ip or not port:
raise ValueError("LocalAI not fully configured")
proto = "https" if self._opt(CONF_LOCALAI_HTTPS, False) else "http"
endpoint = ENDPOINT_LOCALAI.format(protocol=proto, ip_address=ip, port=port)
model = self._current_model("LocalAI")
body = self._openai_compatible_body(
provider="LocalAI",
model=model,
prompt=self._trim_prompt(prompt),
temperature=float(self._opt(CONF_LOCALAI_TEMPERATURE, DEFAULT_TEMPERATURE)),
)
response = await self._post_json(endpoint, body=body, provider_label="LocalAI")
return self._extract_chat_content(response, "LocalAI") if response else None
async def _ollama(self, prompt: str) -> str | None:
ip = self._opt(CONF_OLLAMA_IP_ADDRESS)
port = self._opt(CONF_OLLAMA_PORT)
base = ollama_base_url(
base_url=self._opt(CONF_OLLAMA_BASE_URL),
ip_address=ip,
port=port,
https=self._opt(CONF_OLLAMA_HTTPS, False),
)
if not base:
raise ValueError("Ollama host/port or base URL is not configured")
messages = []
if self._opt(CONF_OLLAMA_DISABLE_THINK, False):
messages.append({"role": "system", "content": "/no_think"})
messages.append({"role": "user", "content": self._trim_prompt(prompt)})
_, out_budget = self._budgets()
body = {
"model": self._current_model("Ollama"),
"messages": messages,
"stream": False,
"options": {
"temperature": float(self._opt(CONF_OLLAMA_TEMPERATURE, DEFAULT_TEMPERATURE)),
"num_predict": out_budget,
},
}
response = None
headers = bearer_auth_headers(self._opt(CONF_OLLAMA_API_KEY))
for endpoint in ollama_api_candidates(base, "api/chat"):
response = await self._post_json(endpoint, headers=headers, body=body, provider_label="Ollama")
if response:
break
if not response:
return None
self._last_response_metadata = {"done_reason": response.get("done_reason"), "usage": response.get("eval_count")}
return response.get("message", {}).get("content")
async def _custom_openai(self, prompt: str) -> str | None:
endpoint = str(self._opt(CONF_CUSTOM_OPENAI_ENDPOINT) or "").rstrip("/")
if not endpoint:
raise ValueError("Custom OpenAI endpoint not configured")
completions_endpoint = openai_chat_endpoint(endpoint)
headers = {"Content-Type": "application/json"}
api_key = self._opt(CONF_CUSTOM_OPENAI_API_KEY)
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
model = self._current_model("Custom OpenAI")
body = self._openai_compatible_body(
provider="Custom OpenAI",
model=model,
prompt=self._trim_prompt(prompt),
temperature=float(self._opt(CONF_CUSTOM_OPENAI_TEMPERATURE, DEFAULT_TEMPERATURE)),
)
response = await self._post_json(completions_endpoint, headers=headers, body=body, provider_label="Custom OpenAI")
return self._extract_chat_content(response, "Custom OpenAI") if response else None
async def _mistral(self, prompt: str) -> str | None:
api_key = self._opt(CONF_MISTRAL_API_KEY)
if not api_key:
raise ValueError("Mistral API key not configured")
model = self._current_model("Mistral AI")
body = self._openai_compatible_body(
provider="Mistral AI",
model=model,
prompt=self._trim_prompt(prompt),
temperature=float(self._opt(CONF_MISTRAL_TEMPERATURE, DEFAULT_TEMPERATURE)),
)
response = await self._post_json(
ENDPOINT_MISTRAL,
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
body=body,
provider_label="Mistral",
)
return self._extract_chat_content(response, "Mistral") if response else None
async def _perplexity(self, prompt: str) -> str | None:
api_key = self._opt(CONF_PERPLEXITY_API_KEY)
if not api_key:
raise ValueError("Perplexity API key not configured")
model = self._current_model("Perplexity AI")
body = self._openai_compatible_body(
provider="Perplexity AI",
model=model,
prompt=self._trim_prompt(prompt),
temperature=float(self._opt(CONF_PERPLEXITY_TEMPERATURE, DEFAULT_TEMPERATURE)),
)
response = await self._post_json(
ENDPOINT_PERPLEXITY,
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "Accept": "application/json"},
body=body,
provider_label="Perplexity",
)
return self._extract_chat_content(response, "Perplexity") if response else None
async def _openrouter(self, prompt: str) -> str | None:
api_key = self._opt(CONF_OPENROUTER_API_KEY)
if not api_key:
raise ValueError("OpenRouter API key not configured")
model = self._current_model("OpenRouter")
extra: dict[str, Any] = {}
reasoning_max_tokens = int(self._opt(CONF_OPENROUTER_REASONING_MAX_TOKENS, 0))
if reasoning_max_tokens > 0:
extra["reasoning"] = {"max_tokens": reasoning_max_tokens}
body = self._openai_compatible_body(
provider="OpenRouter",
model=model,
prompt=self._trim_prompt(prompt),
temperature=float(self._opt(CONF_OPENROUTER_TEMPERATURE, DEFAULT_TEMPERATURE)),
extra=extra,
)
response = await self._post_json(
ENDPOINT_OPENROUTER,
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
body=body,
provider_label="OpenRouter",
)
return self._extract_chat_content(response, "OpenRouter") if response else None
async def _litellm(self, prompt: str) -> str | None:
import litellm
model = self._current_model("LiteLLM")
_, out_budget = self._budgets()
kwargs: dict[str, Any] = {
"model": model,
"messages": [{"role": "user", "content": self._trim_prompt(prompt)}],
"max_tokens": out_budget,
"temperature": float(self._opt(CONF_LITELLM_TEMPERATURE, DEFAULT_TEMPERATURE)),
}
api_key = self._opt(CONF_LITELLM_API_KEY)
if api_key:
kwargs["api_key"] = api_key
api_base = self._opt(CONF_LITELLM_API_BASE)
if api_base:
kwargs["api_base"] = api_base
timeout_seconds = int(self._opt(CONF_REQUEST_TIMEOUT, DEFAULT_REQUEST_TIMEOUT))
kwargs["timeout"] = max(10, timeout_seconds)
response = await litellm.acompletion(**kwargs)
self._last_response_metadata = {
"finish_reason": response.choices[0].finish_reason,
"usage": {
"prompt_tokens": response.usage.prompt_tokens,
"completion_tokens": response.usage.completion_tokens,
"total_tokens": response.usage.total_tokens,
},
}
content = response.choices[0].message.content
if content is None:
raise ValueError("LiteLLM response missing content")
return content
@@ -0,0 +1,108 @@
"""Endpoint normalization helpers for provider setup and requests."""
from __future__ import annotations
import re
def ensure_http_url(value: str | None, *, default_scheme: str = "http") -> str:
"""Return a stripped HTTP(S) URL, adding a scheme when one is omitted."""
endpoint = str(value or "").strip().rstrip("/")
if not endpoint:
return ""
if re.match(r"^https?://", endpoint):
return endpoint
return f"{default_scheme}://{endpoint}"
def openai_chat_endpoint(endpoint: str | None) -> str:
"""Normalize an OpenAI-compatible endpoint to a chat completions URL."""
base = ensure_http_url(endpoint)
if not base:
return ""
if base.endswith("/chat/completions"):
return base
if base.endswith("/api"):
return f"{base}/chat/completions"
if base.endswith("/v1"):
return f"{base}/chat/completions"
return f"{base}/v1/chat/completions"
def openai_model_endpoint_candidates(endpoint: str | None) -> list[str]:
"""Return likely model-listing endpoints for OpenAI-compatible servers."""
base = ensure_http_url(endpoint)
if not base:
return []
if base.endswith("/chat/completions"):
base = base[: -len("/chat/completions")]
candidates: list[str]
if base.endswith("/api"):
candidates = [f"{base}/models", f"{base}/v1/models"]
elif base.endswith("/v1"):
candidates = [f"{base}/models"]
else:
candidates = [f"{base}/v1/models", f"{base}/models", f"{base}/api/models"]
return _dedupe(candidates)
def ollama_base_url(
*,
base_url: str | None = None,
ip_address: str | None = None,
port: int | str | None = None,
https: bool = False,
) -> str:
"""Build an Ollama-compatible base URL from either a full URL or host/port fields."""
if base_url:
return ensure_http_url(base_url)
if not ip_address:
return ""
host = str(ip_address).strip().rstrip("/")
if re.match(r"^https?://", host):
return host
proto = "https" if https else "http"
return f"{proto}://{host}:{port or 11434}"
def ollama_api_candidates(base_url: str, api_path: str) -> list[str]:
"""Return likely Ollama API paths for native Ollama and Open WebUI proxies."""
base = ensure_http_url(base_url)
if not base:
return []
path = api_path.strip("/")
if base.endswith(f"/{path}"):
return [base]
if base.endswith("/api") and path.startswith("api/"):
return [f"{base}/{path.split('/', 1)[1]}"]
candidates = [f"{base}/{path}"]
if not base.endswith("/ollama"):
candidates.append(f"{base}/ollama/{path}")
return _dedupe(candidates)
def bearer_auth_headers(api_key: str | None) -> dict[str, str] | None:
"""Return bearer authorization headers for provider APIs that need a token."""
token = str(api_key or "").strip()
if not token:
return None
if token.lower().startswith("bearer "):
return {"Authorization": token}
return {"Authorization": f"Bearer {token}"}
def _dedupe(values: list[str]) -> list[str]:
seen: set[str] = set()
deduped: list[str] = []
for value in values:
if value not in seen:
seen.add(value)
deduped.append(value)
return deduped
@@ -0,0 +1,38 @@
"""Language helpers for suggestion prompt localization."""
LANGUAGE_NAMES = {
"ca": "Catalan",
"cs": "Czech",
"de": "German",
"en": "English",
"es": "Spanish",
"it": "Italian",
"nl": "Dutch",
"pt": "Portuguese",
"ru": "Russian",
"tr": "Turkish",
"zh": "Chinese",
}
def language_name(language_code: str | None) -> str | None:
"""Return a friendly language name from a Home Assistant language code."""
normalized = str(language_code or "").strip().replace("_", "-").lower()
if not normalized:
return None
base_code = normalized.split("-", 1)[0]
return LANGUAGE_NAMES.get(base_code, normalized)
def suggestion_language_instruction(language_code: str | None) -> str:
"""Build an instruction that asks providers to localize suggestion text."""
name = language_name(language_code)
if not name or name == "English":
return ""
return (
f"Home Assistant configured language: {name}. "
f"Write suggestion titles, descriptions, and warnings in {name}. "
"Keep YAML, entity_ids, service names, and code identifiers unchanged."
)
@@ -0,0 +1,18 @@
{
"domain": "ai_automation_suggester",
"name": "AI Automation Suggester",
"codeowners": ["@ITSpecialist111"],
"config_flow": true,
"dependencies": ["http"],
"documentation": "https://github.com/ITSpecialist111/ai_automation_suggester",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/ITSpecialist111/ai_automation_suggester/issues",
"requirements": [
"anthropic>=0.8.0",
"aiohttp>=3.8.0",
"litellm>=1.67.0",
"pyyaml>=6.0",
"voluptuous>=0.13.1"
],
"version": "1.5.7"
}
@@ -0,0 +1,488 @@
"""Static model capability metadata for AI Automation Suggester.
The catalog is intentionally conservative. Providers still allow custom model
names, but known metadata lets the integration choose safer request parameters
and warn users about stale defaults, preview models, and deprecated IDs.
"""
from __future__ import annotations
from dataclasses import dataclass, field
STATUS_STABLE = "stable"
STATUS_PREVIEW = "preview"
STATUS_DEPRECATED = "deprecated"
STATUS_CUSTOM = "custom"
@dataclass(frozen=True)
class ModelCapabilities:
"""Capabilities for a specific model or model family."""
model: str
label: str = ""
endpoint_family: str = "chat"
context_window: int | None = None
max_output_tokens: int | None = None
token_parameter: str = "max_tokens"
supports_structured_output: bool = False
supports_json_schema: bool = False
supports_reasoning: bool = False
omit_temperature: bool = False
status: str = STATUS_STABLE
notes: tuple[str, ...] = field(default_factory=tuple)
@dataclass(frozen=True)
class ProviderCatalog:
"""Default model and known capabilities for a provider."""
provider: str
default_model: str
models: tuple[ModelCapabilities, ...]
supports_model_listing: bool = False
model_listing_url: str | None = None
SUGGESTION_RESPONSE_SCHEMA: dict = {
"type": "object",
"properties": {
"suggestions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"description": {"type": "string"},
"yaml": {"type": "string"},
"entities_used": {"type": "array", "items": {"type": "string"}},
"automation_ids_used": {"type": "array", "items": {"type": "string"}},
"confidence": {"type": "number"},
"warnings": {"type": "array", "items": {"type": "string"}},
},
"required": ["title", "description", "yaml"],
"additionalProperties": True,
},
}
},
"required": ["suggestions"],
"additionalProperties": True,
}
OPENAI_MODELS = (
ModelCapabilities(
"gpt-5.5",
"GPT-5.5",
endpoint_family="responses",
token_parameter="max_output_tokens",
supports_structured_output=True,
supports_json_schema=True,
supports_reasoning=True,
omit_temperature=True,
notes=("Use the OpenAI Responses API.",),
),
ModelCapabilities(
"gpt-5.5-pro",
"GPT-5.5 Pro",
endpoint_family="responses",
token_parameter="max_output_tokens",
supports_structured_output=True,
supports_json_schema=True,
supports_reasoning=True,
omit_temperature=True,
notes=("Highest-intelligence OpenAI option; expect higher latency and cost.",),
),
ModelCapabilities(
"gpt-5.4-mini",
"GPT-5.4 Mini",
endpoint_family="responses",
token_parameter="max_output_tokens",
supports_structured_output=True,
supports_json_schema=True,
supports_reasoning=True,
omit_temperature=True,
),
ModelCapabilities(
"gpt-5.4",
"GPT-5.4",
endpoint_family="responses",
token_parameter="max_output_tokens",
supports_structured_output=True,
supports_json_schema=True,
supports_reasoning=True,
omit_temperature=True,
),
ModelCapabilities(
"gpt-4o-mini",
"GPT-4o Mini",
token_parameter="max_completion_tokens",
supports_structured_output=True,
supports_json_schema=True,
),
ModelCapabilities(
"gpt-4.1-mini",
"GPT-4.1 Mini",
token_parameter="max_completion_tokens",
supports_structured_output=True,
supports_json_schema=True,
),
ModelCapabilities(
"o3",
"o3",
token_parameter="max_completion_tokens",
supports_reasoning=True,
omit_temperature=True,
),
ModelCapabilities(
"o4-mini",
"o4 Mini",
token_parameter="max_completion_tokens",
supports_reasoning=True,
omit_temperature=True,
),
)
PROVIDER_CATALOGS: dict[str, ProviderCatalog] = {
"OpenAI": ProviderCatalog(
"OpenAI",
"gpt-5.4-mini",
OPENAI_MODELS,
True,
"https://api.openai.com/v1/models",
),
"OpenAI Azure": ProviderCatalog(
"OpenAI Azure",
"gpt-5.4-mini",
OPENAI_MODELS,
),
"Anthropic": ProviderCatalog(
"Anthropic",
"claude-sonnet-4-6",
(
ModelCapabilities(
"claude-opus-4-7",
"Claude Opus 4.7",
token_parameter="max_tokens",
supports_reasoning=True,
notes=("Use adaptive thinking; manual budget_tokens is not accepted.",),
),
ModelCapabilities(
"claude-sonnet-4-6",
"Claude Sonnet 4.6",
token_parameter="max_tokens",
supports_reasoning=True,
notes=("Adaptive thinking is recommended for reasoning-heavy requests.",),
),
ModelCapabilities("claude-haiku-4-5", "Claude Haiku 4.5"),
ModelCapabilities(
"claude-3-7-sonnet-latest",
"Claude 3.7 Sonnet Latest",
status=STATUS_DEPRECATED,
notes=("Prefer Claude Sonnet 4.6 for new configurations.",),
),
),
True,
"https://api.anthropic.com/v1/models",
),
"Google": ProviderCatalog(
"Google",
"gemini-2.5-flash",
(
ModelCapabilities(
"gemini-2.5-flash",
"Gemini 2.5 Flash",
supports_structured_output=True,
supports_json_schema=True,
),
ModelCapabilities(
"gemini-2.5-pro",
"Gemini 2.5 Pro",
supports_structured_output=True,
supports_json_schema=True,
),
ModelCapabilities(
"gemini-3-flash-preview",
"Gemini 3 Flash Preview",
supports_structured_output=True,
supports_json_schema=True,
status=STATUS_PREVIEW,
),
ModelCapabilities(
"gemini-3.1-pro-preview",
"Gemini 3.1 Pro Preview",
supports_structured_output=True,
supports_json_schema=True,
status=STATUS_PREVIEW,
),
ModelCapabilities(
"gemini-2.0-flash",
"Gemini 2.0 Flash",
supports_structured_output=True,
supports_json_schema=True,
status=STATUS_DEPRECATED,
notes=("Gemini 2.0 Flash is deprecated in current Gemini docs.",),
),
),
True,
"https://generativelanguage.googleapis.com/v1beta/models",
),
"Groq": ProviderCatalog(
"Groq",
"llama-3.3-70b-versatile",
(
ModelCapabilities("llama-3.3-70b-versatile", "Llama 3.3 70B Versatile"),
ModelCapabilities("llama-3.1-8b-instant", "Llama 3.1 8B Instant"),
ModelCapabilities("openai/gpt-oss-120b", "GPT OSS 120B"),
ModelCapabilities("openai/gpt-oss-20b", "GPT OSS 20B"),
ModelCapabilities(
"llama3-8b-8192",
"Llama 3 8B 8192",
status=STATUS_DEPRECATED,
notes=("Use llama-3.1-8b-instant or llama-3.3-70b-versatile.",),
),
),
True,
"https://api.groq.com/openai/v1/models",
),
"LocalAI": ProviderCatalog("LocalAI", "llama3", (), True),
"Ollama": ProviderCatalog("Ollama", "llama3.1", (), True),
"Custom OpenAI": ProviderCatalog("Custom OpenAI", "gpt-4o-mini", OPENAI_MODELS, True),
"Generic OpenAI": ProviderCatalog("Generic OpenAI", "gpt-4o-mini", OPENAI_MODELS, True),
"Mistral AI": ProviderCatalog(
"Mistral AI",
"mistral-small-latest",
(
ModelCapabilities(
"mistral-small-latest",
"Mistral Small Latest",
supports_structured_output=True,
supports_json_schema=True,
),
ModelCapabilities(
"mistral-medium-latest",
"Mistral Medium Latest",
supports_structured_output=True,
supports_json_schema=True,
),
ModelCapabilities(
"mistral-large-latest",
"Mistral Large Latest",
supports_structured_output=True,
supports_json_schema=True,
),
ModelCapabilities(
"mistral-medium",
"Mistral Medium",
status=STATUS_DEPRECATED,
notes=("Prefer current -latest aliases or published dated IDs.",),
),
),
True,
"https://api.mistral.ai/v1/models",
),
"Perplexity AI": ProviderCatalog(
"Perplexity AI",
"sonar",
(
ModelCapabilities("sonar", "Sonar", supports_structured_output=True, supports_json_schema=True),
ModelCapabilities(
"sonar-pro",
"Sonar Pro",
supports_structured_output=True,
supports_json_schema=True,
),
ModelCapabilities(
"sonar-reasoning-pro",
"Sonar Reasoning Pro",
supports_structured_output=True,
supports_json_schema=True,
supports_reasoning=True,
),
ModelCapabilities(
"sonar-deep-research",
"Sonar Deep Research",
supports_structured_output=True,
supports_json_schema=True,
supports_reasoning=True,
notes=("Expect higher latency for research workflows.",),
),
),
),
"OpenRouter": ProviderCatalog(
"OpenRouter",
"openai/gpt-5.4-mini",
(
ModelCapabilities(
"openai/gpt-5.5",
"OpenAI GPT-5.5 via OpenRouter",
token_parameter="max_tokens",
supports_structured_output=True,
supports_json_schema=True,
supports_reasoning=True,
omit_temperature=True,
),
ModelCapabilities(
"openai/gpt-5.4-mini",
"OpenAI GPT-5.4 Mini via OpenRouter",
supports_structured_output=True,
supports_json_schema=True,
supports_reasoning=True,
omit_temperature=True,
),
ModelCapabilities(
"anthropic/claude-opus-4.7",
"Claude Opus 4.7 via OpenRouter",
supports_structured_output=True,
supports_json_schema=True,
supports_reasoning=True,
),
ModelCapabilities(
"anthropic/claude-sonnet-4.6",
"Claude Sonnet 4.6 via OpenRouter",
supports_structured_output=True,
supports_json_schema=True,
supports_reasoning=True,
),
),
True,
"https://openrouter.ai/api/v1/models",
),
}
def get_provider_catalog(provider: str) -> ProviderCatalog | None:
"""Return the static catalog for a provider."""
return PROVIDER_CATALOGS.get(provider)
def get_default_model(provider: str) -> str:
"""Return the recommended default model for a provider."""
catalog = get_provider_catalog(provider)
return catalog.default_model if catalog else ""
def get_model_capabilities(provider: str, model: str | None) -> ModelCapabilities:
"""Return known capabilities for a selected model.
Unknown user-supplied model names remain valid and are treated as custom chat
models with conservative OpenAI-compatible defaults.
"""
selected = model or get_default_model(provider)
catalog = get_provider_catalog(provider)
if catalog:
for item in catalog.models:
if item.model == selected:
return item
for item in catalog.models:
if selected.startswith(item.model.rstrip("*")):
return item
if provider in {"OpenAI", "OpenAI Azure", "Custom OpenAI", "Generic OpenAI"}:
if selected.startswith(("gpt-5", "o3", "o4")):
return ModelCapabilities(
selected,
endpoint_family="responses" if provider == "OpenAI" else "chat",
token_parameter="max_output_tokens" if provider == "OpenAI" else "max_completion_tokens",
supports_structured_output=provider == "OpenAI",
supports_json_schema=provider == "OpenAI",
supports_reasoning=True,
omit_temperature=True,
)
if selected.startswith(("gpt-4o", "gpt-4.1")):
return ModelCapabilities(
selected,
token_parameter="max_completion_tokens",
supports_structured_output=True,
supports_json_schema=True,
)
return ModelCapabilities(selected, status=STATUS_CUSTOM)
def model_uses_responses_api(provider: str, model: str | None) -> bool:
"""Return True when the model should use OpenAI's Responses API."""
return provider == "OpenAI" and get_model_capabilities(provider, model).endpoint_family == "responses"
def chat_token_parameter(provider: str, model: str | None) -> str:
"""Return the correct token limit parameter for chat-compatible APIs."""
capabilities = get_model_capabilities(provider, model)
if capabilities.token_parameter == "max_output_tokens":
return "max_completion_tokens"
return capabilities.token_parameter
def should_send_temperature(provider: str, model: str | None) -> bool:
"""Return False for models known to reject custom temperature settings."""
return not get_model_capabilities(provider, model).omit_temperature
def supports_json_schema(provider: str, model: str | None) -> bool:
"""Return True if this provider/model should receive JSON schema requests."""
return get_model_capabilities(provider, model).supports_json_schema
def json_schema_response_format() -> dict:
"""Return an OpenAI-compatible JSON schema response_format block."""
return {
"type": "json_schema",
"json_schema": {
"name": "automation_suggestions",
"strict": False,
"schema": SUGGESTION_RESPONSE_SCHEMA,
},
}
def _strip_schema_keys(value, unsupported_keys: set[str]):
"""Return a copy of a schema value without unsupported keys."""
if isinstance(value, dict):
return {
key: _strip_schema_keys(schema_value, unsupported_keys)
for key, schema_value in value.items()
if key not in unsupported_keys
}
if isinstance(value, list):
return [_strip_schema_keys(item, unsupported_keys) for item in value]
return value
def google_json_schema_response_format() -> dict:
"""Return a Google Gemini-compatible JSON schema response format."""
response_format = json_schema_response_format()
return {
"type": response_format["type"],
"json_schema": {
"name": response_format["json_schema"]["name"],
"strict": response_format["json_schema"]["strict"],
"schema": _strip_schema_keys(
response_format["json_schema"]["schema"],
{"additionalProperties"},
),
},
}
def compatibility_warnings(provider: str, model: str | None) -> list[str]:
"""Return user-facing warnings for the selected provider/model."""
capabilities = get_model_capabilities(provider, model)
warnings: list[str] = []
if capabilities.status == STATUS_PREVIEW:
warnings.append(f"{capabilities.model} is a preview model and may change without notice.")
if capabilities.status == STATUS_DEPRECATED:
warnings.append(f"{capabilities.model} is deprecated; choose a current model when possible.")
warnings.extend(capabilities.notes)
return warnings
@@ -0,0 +1,459 @@
# custom_components/ai_automation_suggester/sensor.py
"""Sensor platform for AI Automation Suggester."""
from __future__ import annotations
import logging
from typing import cast
from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNKNOWN, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import (
DOMAIN,
INTEGRATION_NAME,
CONF_PROVIDER,
PROVIDER_STATUS_CONNECTED,
PROVIDER_STATUS_DISCONNECTED,
PROVIDER_STATUS_ERROR,
PROVIDER_STATUS_INITIALIZING,
CONF_MAX_INPUT_TOKENS,
DEFAULT_MAX_INPUT_TOKENS,
CONF_MAX_OUTPUT_TOKENS,
DEFAULT_MAX_OUTPUT_TOKENS,
# Model configuration keys (used to display current model)
CONF_OPENAI_MODEL,
CONF_ANTHROPIC_MODEL,
CONF_GOOGLE_MODEL,
CONF_GROQ_MODEL,
CONF_LOCALAI_MODEL,
CONF_OLLAMA_MODEL,
CONF_CUSTOM_OPENAI_MODEL,
CONF_MISTRAL_MODEL,
CONF_PERPLEXITY_MODEL,
CONF_OPENROUTER_MODEL,
CONF_OPENAI_AZURE_DEPLOYMENT_ID,
CONF_GENERIC_OPENAI_MODEL,
DEFAULT_MODELS,
# Sensor Keys from const.py
SENSOR_KEY_SUGGESTIONS,
SENSOR_KEY_STATUS,
SENSOR_KEY_INPUT_TOKENS,
SENSOR_KEY_OUTPUT_TOKENS,
SENSOR_KEY_MODEL,
SENSOR_KEY_LAST_ERROR,
SENSOR_KEY_HISTORY_COUNT,
)
_LOGGER = logging.getLogger(__name__)
PROVIDER_TO_MODEL_KEY_MAP: dict[str, str] = {
"OpenAI": CONF_OPENAI_MODEL,
"Anthropic": CONF_ANTHROPIC_MODEL,
"Google": CONF_GOOGLE_MODEL,
"Groq": CONF_GROQ_MODEL,
"LocalAI": CONF_LOCALAI_MODEL,
"Ollama": CONF_OLLAMA_MODEL,
"Custom OpenAI": CONF_CUSTOM_OPENAI_MODEL,
"Mistral AI": CONF_MISTRAL_MODEL,
"Perplexity AI": CONF_PERPLEXITY_MODEL,
"OpenRouter": CONF_OPENROUTER_MODEL,
"OpenAI Azure": CONF_OPENAI_AZURE_DEPLOYMENT_ID,
"Generic OpenAI": CONF_GENERIC_OPENAI_MODEL,
}
SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=SENSOR_KEY_SUGGESTIONS,
name="AI Automation Suggestions",
icon="mdi:robot-happy-outline",
),
SensorEntityDescription(
key=SENSOR_KEY_STATUS,
name="AI Provider Status",
icon="mdi:lan-check",
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key=SENSOR_KEY_INPUT_TOKENS,
name="Max Input Tokens",
icon="mdi:format-letter-starts-with",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement="tokens",
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=SENSOR_KEY_OUTPUT_TOKENS,
name="Max Output Tokens",
icon="mdi:format-letter-ends-with",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement="tokens",
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=SENSOR_KEY_MODEL,
name="AI Model In Use",
icon="mdi:brain",
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key=SENSOR_KEY_LAST_ERROR,
name="Last Error Message",
icon="mdi:alert-circle-outline",
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key=SENSOR_KEY_HISTORY_COUNT,
name="Suggestion History Count",
icon="mdi:history",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AI Automation Suggester sensors from a config entry."""
coordinator = cast(DataUpdateCoordinator, hass.data[DOMAIN][entry.entry_id])
provider_name = entry.data.get(CONF_PROVIDER, "Unknown Provider")
entities: list[SensorEntity] = []
for description in SENSOR_DESCRIPTIONS:
formatted_name = f"{description.name} ({provider_name})"
specific_description = SensorEntityDescription(
key=description.key,
name=formatted_name,
icon=description.icon,
entity_category=description.entity_category,
native_unit_of_measurement=description.native_unit_of_measurement,
state_class=description.state_class,
device_class=description.device_class,
)
if description.key == SENSOR_KEY_SUGGESTIONS:
entities.append(AISuggestionsSensor(coordinator, entry, specific_description))
elif description.key == SENSOR_KEY_STATUS:
entities.append(AIProviderStatusSensor(coordinator, entry, specific_description))
elif description.key == SENSOR_KEY_INPUT_TOKENS:
entities.append(MaxInputTokensSensor(coordinator, entry, specific_description))
elif description.key == SENSOR_KEY_OUTPUT_TOKENS:
entities.append(MaxOutputTokensSensor(coordinator, entry, specific_description))
elif description.key == SENSOR_KEY_MODEL:
entities.append(AIModelSensor(coordinator, entry, specific_description))
elif description.key == SENSOR_KEY_LAST_ERROR:
entities.append(AILastErrorSensor(coordinator, entry, specific_description))
elif description.key == SENSOR_KEY_HISTORY_COUNT:
entities.append(AIHistoryCountSensor(coordinator, entry, specific_description))
else:
entities.append(AIBaseSensor(coordinator, entry, specific_description))
# update_before_add must stay False: in HA 2025.x+, passing True calls
# CoordinatorEntity.async_update() during platform setup, which triggers a
# full LLM inference and exceeds the setup timeout (issue #166). Entities
# populate from coordinator data and refresh on generate_suggestions.
async_add_entities(entities, False)
_LOGGER.debug("Sensor platform setup complete for provider: %s", provider_name)
# ─────────────────────────────────────────────────────────────
# Base sensor
# ─────────────────────────────────────────────────────────────
class AIBaseSensor(CoordinatorEntity[DataUpdateCoordinator], SensorEntity):
"""Base class for AI Automation Suggester sensors."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{entry.entry_id}_{description.key}"
self._entry = entry
self._provider_name = entry.data.get(CONF_PROVIDER, "Unknown Provider")
# Common device info for all sensors of this config entry
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name=f"{INTEGRATION_NAME} ({self._provider_name})",
manufacturer="Community",
model=self._provider_name,
sw_version=str(entry.version) if entry.version else "N/A",
configuration_url=None, # Link Github?
)
@property
def available(self) -> bool:
"""Return True if coordinator is available and has data."""
return super().available and self.coordinator.last_update_success
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if self.coordinator.last_update_success:
self._update_state_and_attributes()
super()._handle_coordinator_update()
def _update_state_and_attributes(self) -> None:
"""Update the sensor's state and attributes based on coordinator data.
This method should be overridden by subclasses.
"""
self._attr_native_value = STATE_UNKNOWN
_LOGGER.debug(
"Sensor %s._update_state_and_attributes not fully implemented for key %s",
self.__class__.__name__,
self.entity_description.key
)
# ─────────────────────────────────────────────────────────────
# Suggestions sensor
# ─────────────────────────────────────────────────────────────
class AISuggestionsSensor(AIBaseSensor):
"""Shows the availability of new AI suggestions."""
_attr_should_poll = False
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
description: SensorEntityDescription,
) -> None:
super().__init__(coordinator, entry, description)
self._previous_suggestions_timestamp: float | None = None
# Initialize state with default values
self._attr_native_value = "No Suggestions"
self._attr_extra_state_attributes = {
"suggestions": "No suggestions yet",
"description": None,
"yaml_block": None,
"last_update": None,
"entities_processed": [],
"provider": self._entry.data.get(CONF_PROVIDER, "unknown"),
"entities_processed_count": 0,
"suggestion_count": 0,
"warnings": [],
}
async def async_added_to_hass(self) -> None:
"""Handle added to Hass."""
await super().async_added_to_hass()
# Update initial state from coordinator if data exists
if self.coordinator.data:
self._update_state_and_attributes()
def _update_state_and_attributes(self) -> None:
"""Update sensor state and attributes."""
data = self.coordinator.data or {}
suggestions = data.get("suggestions")
last_update_timestamp = data.get("last_update")
if suggestions and suggestions not in ("No suggestions available", "No suggestions yet"):
if last_update_timestamp and (self._previous_suggestions_timestamp is None or last_update_timestamp > self._previous_suggestions_timestamp):
self._attr_native_value = "New Suggestions Available"
self._previous_suggestions_timestamp = last_update_timestamp
else:
self._attr_native_value = "Suggestions Available"
else:
self._attr_native_value = "No Suggestions"
self._attr_extra_state_attributes = {
"suggestions": suggestions,
"description": data.get("description"),
"yaml_block": data.get("yaml_block"),
"last_update": data.get("last_update"),
"entities_processed": data.get("entities_processed", []),
"provider": self._entry.data.get(CONF_PROVIDER, "unknown"),
"model": data.get("model"),
"entities_processed_count": len(data.get("entities_processed", [])),
"suggestion": data.get("suggestion"),
"suggestion_count": data.get("suggestion_count", 0),
"warnings": data.get("warnings", []),
}
# ─────────────────────────────────────────────────────────────
# Providerstatus sensor
# ─────────────────────────────────────────────────────────────
class AIProviderStatusSensor(AIBaseSensor):
"""Indicates whether the configured provider is reachable."""
_attr_should_poll = False
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
description: SensorEntityDescription,
) -> None:
super().__init__(coordinator, entry, description)
self._update_state_and_attributes()
def _update_state_and_attributes(self) -> None:
"""Update sensor state and attributes."""
data = self.coordinator.data or {}
if not self.coordinator.last_update_success:
self._attr_native_value = PROVIDER_STATUS_ERROR
elif not data:
self._attr_native_value = PROVIDER_STATUS_INITIALIZING
elif data.get("last_error"):
self._attr_native_value = PROVIDER_STATUS_ERROR
elif "suggestions" in data:
self._attr_native_value = PROVIDER_STATUS_CONNECTED
else:
self._attr_native_value = PROVIDER_STATUS_DISCONNECTED
self._attr_extra_state_attributes = {
"last_error_message": data.get("last_error", None),
"last_attempted_update": data.get("last_update"),
"provider": data.get("provider", self._provider_name),
"model": data.get("model"),
"warnings": data.get("warnings", []),
"response_metadata": data.get("response_metadata", {}),
}
# ─────────────────────────────────────────────────────────────
# Max Input Token Sensors
# ─────────────────────────────────────────────────────────────
class MaxInputTokensSensor(AIBaseSensor):
"""Shows the configured maximum input tokens."""
_attr_should_poll = False
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
description: SensorEntityDescription,
) -> None:
super().__init__(coordinator, entry, description)
self._update_state_and_attributes() # Initial update
def _update_state_and_attributes(self) -> None:
"""Update sensor state from config entry options or data."""
self._attr_native_value = self._entry.options.get(
CONF_MAX_INPUT_TOKENS,
self._entry.data.get(CONF_MAX_INPUT_TOKENS, DEFAULT_MAX_INPUT_TOKENS)
)
# ─────────────────────────────────────────────────────────────
# Max Output Token Sensors
# ─────────────────────────────────────────────────────────────
class MaxOutputTokensSensor(AIBaseSensor):
"""Shows the configured maximum output tokens."""
_attr_should_poll = False
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
description: SensorEntityDescription,
) -> None:
super().__init__(coordinator, entry, description)
self._update_state_and_attributes() # Initial update
def _update_state_and_attributes(self) -> None:
"""Update sensor state from config entry options or data."""
self._attr_native_value = self._entry.options.get(
CONF_MAX_OUTPUT_TOKENS,
self._entry.data.get(CONF_MAX_OUTPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS)
)
# ─────────────────────────────────────────────────────────────
# Model Sensor
# ─────────────────────────────────────────────────────────────
class AIModelSensor(AIBaseSensor):
"""Shows the currently configured AI model."""
_attr_should_poll = False
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
description: SensorEntityDescription,
) -> None:
super().__init__(coordinator, entry, description)
self._update_state_and_attributes()
def _update_state_and_attributes(self) -> None:
"""Update sensor state with the configured model."""
provider = self._entry.data.get(CONF_PROVIDER)
if not provider:
self._attr_native_value = STATE_UNKNOWN
return
model_key = PROVIDER_TO_MODEL_KEY_MAP.get(provider)
if not model_key:
self._attr_native_value = "Unknown Model Key"
_LOGGER.warning("No model key found for provider: %s", provider)
return
self._attr_native_value = self._entry.options.get(
model_key,
self._entry.data.get(model_key, DEFAULT_MODELS.get(provider, "unknown"))
) if model_key else "unknown"
# ─────────────────────────────────────────────────────────────
# Last Error sensor
# ─────────────────────────────────────────────────────────────
class AILastErrorSensor(AIBaseSensor):
"""Shows the last error message from the AI provider."""
_attr_should_poll = False
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
description: SensorEntityDescription,
) -> None:
super().__init__(coordinator, entry, description)
self._update_state_and_attributes() # Initial update
def _update_state_and_attributes(self) -> None:
"""Update sensor state with the last error message."""
data = self.coordinator.data or {}
last_error = data.get("last_error")
self._attr_native_value = str(last_error) if last_error else "No Error"
self._attr_extra_state_attributes = {
"last_error_timestamp": data.get("last_update") if last_error else None,
}
class AIHistoryCountSensor(AIBaseSensor):
"""Shows how many suggestions are retained in history."""
_attr_should_poll = False
def __init__(
self,
coordinator: DataUpdateCoordinator,
entry: ConfigEntry,
description: SensorEntityDescription,
) -> None:
super().__init__(coordinator, entry, description)
self._update_state_and_attributes()
def _update_state_and_attributes(self) -> None:
"""Update state from coordinator history data."""
data = self.coordinator.data or {}
self._attr_native_value = data.get("suggestion_count", 0)
self._attr_extra_state_attributes = {
"latest_suggestion_id": (data.get("suggestion") or {}).get("id"),
"latest_suggestion_status": (data.get("suggestion") or {}).get("status"),
}
@@ -0,0 +1,132 @@
generate_suggestions:
name: Generate Suggestions
description: "Manually trigger AI automation suggestions."
fields:
provider_config:
name: Provider Configuration
description: Which provider configuration to use (if you have multiple configured)
required: false
selector:
config_entry:
integration: ai_automation_suggester
custom_prompt:
name: Custom Prompt
description: Optional custom prompt to override or enhance the default system prompt.
required: false
example: "Focus on energy-saving automations"
selector:
text:
multiline: true
all_entities:
name: Consider All Entities
description: "If true, consider all entities instead of just new entities."
required: false
default: false
example: false
selector:
boolean: {}
domains:
name: Domains
description: "List of domains to consider. If empty, consider all domains."
required: false
default: []
selector:
select:
multiple: true
custom_value: true
options:
- light
- switch
- sensor
- binary_sensor
- climate
- cover
- media_player
- lock
- alarm_control_panel
exclude_domains:
name: Exclude Domains
description: "Domains to exclude from analysis, even when all entities are considered."
required: false
default: []
selector:
select:
multiple: true
custom_value: true
options:
- automation
- update
- button
- event
exclude_entities:
name: Exclude Entities
description: "Specific entity IDs to exclude from analysis."
required: false
default: []
selector:
entity:
multiple: true
exclude_areas:
name: Exclude Areas
description: "Area IDs or area names to exclude from analysis."
required: false
default: []
selector:
area:
multiple: true
entity_limit:
name: Entity Limit
description: "Maximum number of entities to consider (randomly selected from the chosen domains)."
required: false
default: 200
example: 200
selector:
number:
min: 50
max: 1000
mode: slider
automation_read_yaml:
name: Read 'automations.yaml' file.
description: "Reads and appends the yaml code of the automations found in the automations.yaml file. This action will use a lot of input tokens, use it with care and with models with a large input window (e.g. Gemini)."
required: false
default: false
example: false
selector:
boolean: {}
automation_limit:
name: Automation Limit
description: "Maximum number of automations to analyze (default: 100)."
required: false
default: 100
example: 100
selector:
number:
min: 10
max: 500
mode: slider
clear_history:
name: Clear Suggestion History
description: "Clear all stored AI automation suggestions."
update_suggestion:
name: Update Suggestion
description: "Update the review status for a stored AI automation suggestion."
fields:
suggestion_id:
name: Suggestion ID
description: "The suggestion ID to update."
required: true
selector:
text: {}
status:
name: Status
description: "New review status for the suggestion."
required: true
selector:
select:
options:
- new
- accepted
- declined
- dismissed
@@ -0,0 +1,79 @@
"""Persistent suggestion history storage."""
from __future__ import annotations
from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.helpers.storage import Store
from .const import DEFAULT_HISTORY_RETENTION, DOMAIN
STORE_VERSION = 1
STORE_KEY = f"{DOMAIN}.suggestions"
STORE_DATA_KEY = "suggestions"
HASS_STORE_KEY = "_suggestion_store"
class SuggestionStore:
"""Small wrapper around Home Assistant storage helper."""
def __init__(self, hass: HomeAssistant) -> None:
self._store: Store[dict[str, Any]] = Store(hass, STORE_VERSION, STORE_KEY)
self._data: dict[str, Any] | None = None
async def _async_load(self) -> dict[str, Any]:
if self._data is None:
self._data = await self._store.async_load() or {STORE_DATA_KEY: []}
self._data.setdefault(STORE_DATA_KEY, [])
return self._data
async def async_list(self) -> list[dict[str, Any]]:
"""Return stored suggestions, newest first."""
data = await self._async_load()
return list(data.get(STORE_DATA_KEY, []))
async def async_add_suggestions(
self,
suggestions: list[dict[str, Any]],
retention: int = DEFAULT_HISTORY_RETENTION,
) -> list[dict[str, Any]]:
"""Persist suggestions and return the new stored list."""
data = await self._async_load()
current = list(data.get(STORE_DATA_KEY, []))
data[STORE_DATA_KEY] = suggestions + current
if retention > 0:
data[STORE_DATA_KEY] = data[STORE_DATA_KEY][:retention]
await self._store.async_save(data)
return list(data[STORE_DATA_KEY])
async def async_update_status(self, suggestion_id: str, status: str) -> dict[str, Any] | None:
"""Update a suggestion status such as accepted, declined, or dismissed."""
data = await self._async_load()
for suggestion in data.get(STORE_DATA_KEY, []):
if suggestion.get("id") == suggestion_id:
suggestion["status"] = status
await self._store.async_save(data)
return suggestion
return None
async def async_clear(self) -> None:
"""Clear all stored suggestions."""
data = await self._async_load()
data[STORE_DATA_KEY] = []
await self._store.async_save(data)
def async_get_suggestion_store(hass: HomeAssistant) -> SuggestionStore:
"""Return the singleton suggestion store for this Home Assistant instance."""
domain_data = hass.data.setdefault(DOMAIN, {})
store = domain_data.get(HASS_STORE_KEY)
if store is None:
store = SuggestionStore(hass)
domain_data[HASS_STORE_KEY] = store
return store
@@ -0,0 +1,308 @@
{
"title": "AI Automation Suggester",
"config": {
"step": {
"user": {
"title": "Select AI Provider",
"data": {
"provider": "AI Provider"
}
},
"openai": {
"title": "Configure OpenAI",
"data": {
"openai_api_key": "OpenAI API Key",
"openai_model": "OpenAI Model",
"openai_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"anthropic": {
"title": "Configure Anthropic",
"data": {
"anthropic_api_key": "Anthropic API Key",
"anthropic_model": "Anthropic Model",
"anthropic_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"google": {
"title": "Configure Google",
"data": {
"google_api_key": "Google API Key",
"google_model": "Google Model",
"google_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"groq": {
"title": "Configure Groq",
"data": {
"groq_api_key": "Groq API Key",
"groq_model": "Groq Model",
"groq_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"localai": {
"title": "Configure LocalAI",
"data": {
"localai_ip": "LocalAI IP Address",
"localai_port": "LocalAI Port",
"localai_https": "Use HTTPS for LocalAI",
"localai_model": "LocalAI Model",
"localai_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"ollama": {
"title": "Configure Ollama",
"data": {
"ollama_base_url": "Ollama/Open WebUI Base URL",
"ollama_api_key": "Ollama/Open WebUI API Key (optional)",
"ollama_ip": "Ollama IP Address",
"ollama_port": "Ollama Port",
"ollama_https": "Use HTTPS for Ollama",
"ollama_model": "Ollama Model",
"ollama_temperature": "Temperature",
"ollama_disable_think": "Disable Think Mode (Ollama)",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"custom_openai": {
"title": "Configure Custom OpenAI",
"data": {
"custom_openai_endpoint": "Custom OpenAI Endpoint",
"custom_openai_api_key": "Custom OpenAI API Key (Optional)",
"custom_openai_model": "Custom OpenAI Model",
"custom_openai_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"mistral": {
"title": "Configure Mistral AI",
"data": {
"mistral_api_key": "Mistral API Key",
"mistral_model": "Mistral Model",
"mistral_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"perplexity": {
"title": "Configure Perplexity AI",
"data": {
"perplexity_api_key": "Perplexity API Key",
"perplexity_model": "Perplexity Model",
"perplexity_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"openrouter": {
"title": "Configure OpenRouter",
"data": {
"openrouter_api_key": "OpenRouter API Key",
"openrouter_model": "OpenRouter Model",
"openrouter_reasoning_max_tokens": "OpenRouter Reasoning Max Tokens",
"openrouter_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"openai_azure": {
"title": "Configure OpenAI Azure",
"data": {
"openai_azure_api_key": "Azure OpenAI API Key",
"openai_azure_deployment_id": "Azure Deployment ID",
"openai_azure_endpoint": "Azure Endpoint",
"openai_azure_api_version": "Azure API Version",
"openai_azure_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"generic_openai": {
"title": "Configure Generic OpenAI",
"data": {
"generic_openai_api_endpoint": "Generic OpenAI API Full URL (should end with 'completions')",
"generic_openai_api_key": "Generic OpenAI API Key (Optional)",
"generic_openai_model": "Generic OpenAI Model",
"generic_openai_temperature": "Temperature",
"generic_openai_validation_endpoint": "Validation URL (should end with 'models')",
"generic_openai_enable_validation": "Enable Validation",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"litellm": {
"title": "Configure LiteLLM",
"data": {
"litellm_model": "LiteLLM Model (e.g. openai/gpt-4o, anthropic/claude-sonnet-4-6, groq/llama-3.3-70b-versatile)",
"litellm_api_key": "API Key (optional if set via environment variable)",
"litellm_api_base": "API Base URL (optional, for LiteLLM proxy)",
"litellm_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
}
},
"error": {
"api_error": "API validation failed: {error_message}",
"cannot_connect": "Failed to connect. Please check your settings and network.",
"unknown": "An unknown error occurred. Please check logs for details."
},
"abort": {
"already_configured": "This AI provider is already configured. You can edit it from the integrations page."
}
},
"options": {
"step": {
"init": {
"title": "AI Automation Suggester Options",
"data": {
"openai_api_key": "OpenAI API Key",
"openai_model": "OpenAI Model",
"openai_temperature": "Temperature (OpenAI)",
"anthropic_api_key": "Anthropic API Key",
"anthropic_model": "Anthropic Model",
"anthropic_temperature": "Temperature (Anthropic)",
"google_api_key": "Google API Key",
"google_model": "Google Model",
"google_temperature": "Temperature (Google)",
"groq_api_key": "Groq API Key",
"groq_model": "Groq Model",
"groq_temperature": "Temperature (Groq)",
"localai_ip": "LocalAI IP Address",
"localai_port": "LocalAI Port",
"localai_https": "Use HTTPS for LocalAI",
"localai_model": "LocalAI Model",
"localai_temperature": "Temperature (LocalAI)",
"ollama_base_url": "Ollama/Open WebUI Base URL",
"ollama_api_key": "Ollama/Open WebUI API Key (optional)",
"ollama_ip": "Ollama IP Address",
"ollama_port": "Ollama Port",
"ollama_https": "Use HTTPS for Ollama",
"ollama_model": "Ollama Model",
"ollama_temperature": "Temperature (Ollama)",
"ollama_disable_think": "Disable Think Mode (Ollama)",
"custom_openai_endpoint": "Custom OpenAI Endpoint",
"custom_openai_api_key": "Custom OpenAI API Key",
"custom_openai_model": "Custom OpenAI Model",
"custom_openai_temperature": "Temperature (Custom OpenAI)",
"mistral_api_key": "Mistral API Key",
"mistral_model": "Mistral Model",
"mistral_temperature": "Temperature (Mistral AI)",
"perplexity_api_key": "Perplexity API Key",
"perplexity_model": "Perplexity Model",
"perplexity_temperature": "Temperature (Perplexity AI)",
"openrouter_api_key": "OpenRouter API Key",
"openrouter_model": "OpenRouter Model",
"openrouter_reasoning_max_tokens": "OpenRouter Reasoning Max Tokens",
"openrouter_temperature": "Temperature (OpenRouter)",
"openai_azure_api_key": "Azure OpenAI API Key",
"openai_azure_deployment_id": "Azure Deployment ID",
"openai_azure_endpoint": "Azure Endpoint",
"openai_azure_api_version": "Azure API Version",
"openai_azure_temperature": "Temperature (OpenAI Azure)",
"generic_openai_api_endpoint": "Generic OpenAI API Full URL (should end with 'completions')",
"generic_openai_api_key": "Generic OpenAI API Key",
"generic_openai_model": "Generic OpenAI Model",
"generic_openai_temperature": "Temperature (Generic OpenAI)",
"generic_openai_validation_endpoint": "Validation URL (Generic OpenAI, should end with 'models')",
"generic_openai_enable_validation": "Enable Validation (Generic OpenAI)",
"litellm_api_key": "LiteLLM API Key",
"litellm_model": "LiteLLM Model",
"litellm_api_base": "LiteLLM API Base URL",
"litellm_temperature": "Temperature (LiteLLM)",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens",
"custom_system_prompt": "Persistent Custom System Prompt",
"excluded_domains": "Excluded Domains",
"excluded_entities": "Excluded Entities",
"excluded_areas": "Excluded Areas",
"history_retention": "Suggestion History Retention",
"request_timeout": "Provider Request Timeout",
"openai_reasoning_effort": "OpenAI Reasoning Effort"
},
"description": "Adjust settings for your AI providers. Fields relevant to your configured provider will be used. Common settings like token limits are configured per provider."
}
},
"error": {
"invalid_input": "One or more values are invalid."
}
},
"services": {
"generate_suggestions": {
"name": "Generate Suggestions",
"description": "Manually trigger AI automation suggestions.",
"fields": {
"provider_config": {
"name": "Provider Configuration",
"description": "Which provider configuration to use (if you have multiple)"
},
"custom_prompt": {
"name": "Custom Prompt",
"description": "Optional custom prompt to override the default system prompt or guide the suggestions towards specific themes"
},
"all_entities": {
"name": "Consider All Entities",
"description": "If true, consider all entities instead of just new entities."
},
"domains": {
"name": "Domains",
"description": "List of domains to consider. If empty, consider all domains."
},
"entity_limit": {
"name": "Entity Limit",
"description": "Maximum number of entities to consider (randomly selected)."
},
"automation_read_yaml": {
"name": "Read 'automations.yaml' file",
"description": "Reads and appends the YAML code of the automations found in the 'automations.yaml' file. This action will use a lot of input tokens, use it with care and with models with a large context window (e.g. Gemini)."
},
"automation_limit": {
"name": "Automation Limit",
"description": "Maximum number of automations to analyze (default: 100)."
},
"exclude_domains": {
"name": "Exclude Domains",
"description": "Domains to exclude from analysis."
},
"exclude_entities": {
"name": "Exclude Entities",
"description": "Entity IDs to exclude from analysis."
},
"exclude_areas": {
"name": "Exclude Areas",
"description": "Areas to exclude from analysis."
}
}
},
"clear_history": {
"name": "Clear Suggestion History",
"description": "Clear all stored AI automation suggestions."
},
"update_suggestion": {
"name": "Update Suggestion",
"description": "Update the review status for a stored AI automation suggestion.",
"fields": {
"suggestion_id": {
"name": "Suggestion ID",
"description": "The suggestion ID to update."
},
"status": {
"name": "Status",
"description": "The new review status."
}
}
}
}
}
@@ -0,0 +1,331 @@
"""Suggestion parsing and formatting helpers."""
from __future__ import annotations
import json
import re
import textwrap
from datetime import datetime
from typing import Any
from uuid import uuid4
import yaml
YAML_RE = re.compile(r"```(?:yaml|yml)\s*([\s\S]+?)\s*```", flags=re.IGNORECASE)
JSON_RE = re.compile(r"```json\s*([\s\S]+?)\s*```", flags=re.IGNORECASE)
STRING_FIELDS_AFTER_YAML = "entities_used|automation_ids_used|confidence|warnings"
PARSE_REPAIR_WARNING = "The provider returned malformed JSON; suggestions were parsed best-effort."
STRUCTURED_OUTPUT_INSTRUCTIONS = """
Return a JSON object with this shape and no surrounding Markdown:
{
"suggestions": [
{
"title": "Short automation title",
"description": "Why this automation is useful and how it works",
"yaml": "Home Assistant automation YAML",
"entities_used": ["domain.entity_id"],
"automation_ids_used": [],
"confidence": 0.0,
"warnings": []
}
]
}
Only reference entity_ids present in the prompt. Keep suggestions review-only;
do not claim that automations have been created or changed.
"""
def _as_list(value: Any) -> list:
if value is None:
return []
if isinstance(value, list):
return value
return [value]
def _try_json_loads(raw_response: str) -> dict | list | None:
"""Try to decode model output as JSON, including fenced JSON."""
text = raw_response.strip()
fenced = JSON_RE.search(text)
if fenced:
text = fenced.group(1).strip()
try:
return json.loads(text)
except json.JSONDecodeError:
start = text.find("{")
end = text.rfind("}")
if start != -1 and end != -1 and end > start:
try:
return json.loads(text[start : end + 1])
except json.JSONDecodeError:
return None
return None
def _decode_jsonish_string(value: str) -> str:
"""Decode a JSON string fragment when possible."""
try:
return str(json.loads(f'"{value}"'))
except json.JSONDecodeError:
return value.replace('\\"', '"').replace("\\n", "\n").strip()
def _extract_string_field(segment: str, field: str) -> str | None:
match = re.search(rf'"{re.escape(field)}"\s*:\s*"((?:\\.|[^"\\])*)"', segment)
return _decode_jsonish_string(match.group(1)).strip() if match else None
def _extract_array_field(segment: str, field: str) -> list:
match = re.search(rf'"{re.escape(field)}"\s*:\s*(\[[\s\S]*?\])', segment)
if not match:
return []
try:
value = json.loads(match.group(1))
except json.JSONDecodeError:
return []
return value if isinstance(value, list) else []
def _extract_number_field(segment: str, field: str) -> float | None:
match = re.search(rf'"{re.escape(field)}"\s*:\s*(-?\d+(?:\.\d+)?)', segment)
return float(match.group(1)) if match else None
def _extract_yaml_field(segment: str) -> str | None:
malformed = re.search(
rf'"yaml"\s*:\s*""\s*\r?\n(?P<yaml>[\s\S]*?)\r?\n\s*""\s*,?\s*(?=\r?\n\s*"(?:{STRING_FIELDS_AFTER_YAML})"|\r?\n\s*\}})',
segment,
)
if malformed:
return textwrap.dedent(malformed.group("yaml")).strip() or None
valid = re.search(r'"yaml"\s*:\s*"((?:\\.|[^"\\])*)"', segment)
if valid:
return _decode_jsonish_string(valid.group(1)).strip() or None
return None
def _try_loose_structured_items(raw_response: str) -> list[dict[str, Any]]:
"""Extract suggestions from malformed JSON-like provider responses."""
if '"suggestions"' not in raw_response and '"title"' not in raw_response:
return []
title_matches = list(re.finditer(r'"title"\s*:', raw_response))
items: list[dict[str, Any]] = []
for index, match in enumerate(title_matches):
start = raw_response.rfind("{", 0, match.start())
if start == -1:
start = match.start()
end = title_matches[index + 1].start() if index + 1 < len(title_matches) else len(raw_response)
segment = raw_response[start:end]
title = _extract_string_field(segment, "title")
description = _extract_string_field(segment, "description")
yaml_code = _extract_yaml_field(segment)
if not any((title, description, yaml_code)):
continue
item: dict[str, Any] = {
"title": title,
"description": description,
"yaml": yaml_code,
"entities_used": _extract_array_field(segment, "entities_used"),
"automation_ids_used": _extract_array_field(segment, "automation_ids_used"),
"warnings": _extract_array_field(segment, "warnings"),
}
confidence = _extract_number_field(segment, "confidence")
if confidence is not None:
item["confidence"] = confidence
items.append(item)
return items
def _validate_yaml(yaml_code: str | None) -> list[str]:
warnings: list[str] = []
if not yaml_code:
warnings.append("No automation YAML was returned.")
return warnings
try:
parsed = yaml.safe_load(yaml_code)
except yaml.YAMLError as err:
warnings.append(f"Returned YAML could not be parsed: {err}")
return warnings
if parsed is None:
warnings.append("Returned YAML was empty after parsing.")
elif not isinstance(parsed, (dict, list)):
warnings.append("Returned YAML parsed to an unexpected scalar value.")
return warnings
def _normalise_suggestion(
item: dict[str, Any],
*,
provider: str,
model: str,
created_at: datetime,
entities_processed: list[str],
inherited_warnings: list[str],
response_metadata: dict[str, Any],
) -> dict[str, Any]:
title = str(item.get("title") or "AI automation suggestion").strip()
description = str(item.get("description") or item.get("shortDescription") or "").strip()
yaml_code = item.get("yaml") or item.get("yaml_block") or item.get("yamlCode")
yaml_code = str(yaml_code).strip() if yaml_code else None
warnings = [str(w) for w in inherited_warnings]
warnings.extend(str(w) for w in _as_list(item.get("warnings")))
warnings.extend(_validate_yaml(yaml_code))
finish_reason = response_metadata.get("finish_reason")
if finish_reason in {"length", "max_tokens"}:
warnings.append("The provider reported a length finish reason; the suggestion may be truncated.")
if response_metadata.get("status") == "incomplete":
warnings.append("The provider returned an incomplete response.")
suggestion_id = str(item.get("id") or uuid4())
return {
"id": suggestion_id,
"title": title,
"shortDescription": description[:180] if description else title,
"detailedDescription": description,
"description": description,
"yamlCode": yaml_code,
"yaml_block": yaml_code,
"provider": provider,
"model": model,
"status": str(item.get("status") or "new"),
"created_at": created_at.isoformat(),
"entities_used": [str(e) for e in _as_list(item.get("entities_used"))],
"automation_ids_used": [str(a) for a in _as_list(item.get("automation_ids_used"))],
"entities_processed": entities_processed,
"confidence": item.get("confidence"),
"warnings": warnings,
"response_metadata": response_metadata,
}
def parse_suggestion_response(
raw_response: str,
*,
provider: str,
model: str,
created_at: datetime,
entities_processed: list[str],
inherited_warnings: list[str] | None = None,
response_metadata: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
"""Parse a provider response into stored suggestion dictionaries."""
inherited = inherited_warnings or []
metadata = response_metadata or {}
structured = _try_json_loads(raw_response)
if isinstance(structured, dict):
raw_items = structured.get("suggestions")
if raw_items is None:
raw_items = [structured]
elif isinstance(raw_items, dict):
raw_items = [raw_items]
elif not isinstance(raw_items, list):
raw_items = []
suggestions = [
_normalise_suggestion(
item if isinstance(item, dict) else {"description": str(item)},
provider=provider,
model=model,
created_at=created_at,
entities_processed=entities_processed,
inherited_warnings=inherited,
response_metadata=metadata,
)
for item in raw_items
]
if suggestions:
return suggestions
elif isinstance(structured, list):
suggestions = [
_normalise_suggestion(
item if isinstance(item, dict) else {"description": str(item)},
provider=provider,
model=model,
created_at=created_at,
entities_processed=entities_processed,
inherited_warnings=inherited,
response_metadata=metadata,
)
for item in structured
]
if suggestions:
return suggestions
loose_items = _try_loose_structured_items(raw_response)
if loose_items:
loose_warnings = [*inherited, PARSE_REPAIR_WARNING]
return [
_normalise_suggestion(
item,
provider=provider,
model=model,
created_at=created_at,
entities_processed=entities_processed,
inherited_warnings=loose_warnings,
response_metadata=metadata,
)
for item in loose_items
]
yaml_match = YAML_RE.search(raw_response)
yaml_code = yaml_match.group(1).strip() if yaml_match else None
if yaml_match:
description = YAML_RE.sub("", raw_response).strip()
elif raw_response.lstrip().startswith(("{", "[")) or '"suggestions"' in raw_response:
description = "The provider returned structured output that could not be parsed. Try regenerating with a lower entity limit or a newer model."
else:
description = raw_response.strip()
return [
_normalise_suggestion(
{
"title": "AI automation suggestion",
"description": description,
"yaml": yaml_code,
},
provider=provider,
model=model,
created_at=created_at,
entities_processed=entities_processed,
inherited_warnings=inherited,
response_metadata=metadata,
)
]
def format_suggestion_notification(suggestion: dict[str, Any]) -> str:
"""Render one suggestion for a Home Assistant notification."""
parts = [f"## {suggestion.get('title', 'AI automation suggestion')}"]
description = suggestion.get("description")
if description:
parts.append(str(description))
yaml_code = suggestion.get("yamlCode") or suggestion.get("yaml_block")
if yaml_code:
parts.append("```yaml\n" + str(yaml_code).strip() + "\n```")
warnings = suggestion.get("warnings") or []
if warnings:
parts.append("Warnings:\n" + "\n".join(f"- {_format_notification_warning(warning)}" for warning in warnings))
return "\n\n".join(parts)
def _format_notification_warning(warning: Any) -> str:
"""Return a concise user-facing warning for notifications."""
text = str(warning)
if text == PARSE_REPAIR_WARNING:
return "Provider response needed formatting repair before display. Review the YAML before using it."
if text == "The provider reported a length finish reason; the suggestion may be truncated.":
return "The AI response may have been cut off. Review the YAML before using it."
return text
@@ -0,0 +1,252 @@
{
"title": "AI Automation Suggester",
"config": {
"step": {
"user": {
"title": "Seleccionar Proveïdor d'IA",
"data": {
"provider": "Proveïdor d'IA"
}
},
"openai": {
"title": "Configurar OpenAI",
"data": {
"openai_api_key": "Clau API d'OpenAI",
"openai_model": "Model d'OpenAI",
"openai_temperature": "Temperatura",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
},
"anthropic": {
"title": "Configurar Anthropic",
"data": {
"anthropic_api_key": "Clau API d'Anthropic",
"anthropic_model": "Model d'Anthropic",
"anthropic_temperature": "Temperatura",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
},
"google": {
"title": "Configurar Google",
"data": {
"google_api_key": "Clau API de Google",
"google_model": "Model de Google",
"google_temperature": "Temperatura",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
},
"groq": {
"title": "Configurar Groq",
"data": {
"groq_api_key": "Clau API de Groq",
"groq_model": "Model de Groq",
"groq_temperature": "Temperatura",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
},
"localai": {
"title": "Configurar LocalAI",
"data": {
"localai_ip": "Adreça IP de LocalAI",
"localai_port": "Port de LocalAI",
"localai_https": "Utilitzar HTTPS per a LocalAI",
"localai_model": "Model de LocalAI",
"localai_temperature": "Temperatura",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
},
"ollama": {
"title": "Configurar Ollama",
"data": {
"ollama_ip": "Adreça IP d'Ollama",
"ollama_port": "Port d'Ollama",
"ollama_https": "Utilitzar HTTPS per a Ollama",
"ollama_model": "Model d'Ollama",
"ollama_temperature": "Temperatura",
"ollama_disable_think": "Desactivar Mode de Pensament (Ollama)",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
},
"custom_openai": {
"title": "Configurar OpenAI Personalitzat",
"data": {
"custom_openai_endpoint": "URL de l'API d'OpenAI Personalitzada",
"custom_openai_api_key": "Clau API d'OpenAI Personalitzada (Opcional)",
"custom_openai_model": "Model d'OpenAI Personalitzat",
"custom_openai_temperature": "Temperatura",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
},
"mistral": {
"title": "Configurar Mistral AI",
"data": {
"mistral_api_key": "Clau API de Mistral",
"mistral_model": "Model de Mistral",
"mistral_temperature": "Temperatura",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
},
"perplexity": {
"title": "Configurar Perplexity AI",
"data": {
"perplexity_api_key": "Clau API de Perplexity",
"perplexity_model": "Model de Perplexity",
"perplexity_temperature": "Temperatura",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
},
"openrouter": {
"title": "Configurar OpenRouter",
"data": {
"openrouter_api_key": "Clau API d'OpenRouter",
"openrouter_model": "Model d'OpenRouter",
"openrouter_reasoning_max_tokens": "Màxim de tokens de raonament d'OpenRouter",
"openrouter_temperature": "Temperatura",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
},
"openai_azure": {
"title": "Configurar OpenAI Azure",
"data": {
"openai_azure_api_key": "Clau API d'Azure OpenAI",
"openai_azure_deployment_id": "ID de Desplegament d'Azure",
"openai_azure_endpoint": "URL de l'API d'Azure",
"openai_azure_api_version": "Versió de l'API d'Azure",
"openai_azure_temperature": "Temperatura",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
},
"generic_openai": {
"title": "Configurar OpenAI Genèric",
"data": {
"generic_openai_api_endpoint": "URL completa de l'API d'OpenAI genèrica (hauria d'acabar amb 'completions')",
"generic_openai_api_key": "Clau API d'OpenAI Genèric (Opcional)",
"generic_openai_model": "Model d'OpenAI Genèric",
"generic_openai_temperature": "Temperatura",
"generic_openai_validation_endpoint": "URL de validació (hauria d'acabar amb 'models')",
"generic_openai_enable_validation": "Activar Validació",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
}
}
},
"error": {
"api_error": "La validació de l'API ha fallat: {error_message}",
"cannot_connect": "No s'ha pogut connectar. Revisa la teva configuració i la xarxa.",
"unknown": "S'ha produït un error desconegut. Revisa els registres per a més detalls."
},
"abort": {
"already_configured": "Aquest proveïdor d'IA ja està configurat. Pots editar-lo des de la pàgina d'integracions."
}
},
"options": {
"step": {
"init": {
"title": "Opcions del Suggeridor d'Automatitzacions amb IA",
"data": {
"openai_api_key": "Clau API d'OpenAI",
"openai_model": "Model d'OpenAI",
"openai_temperature": "Temperatura (OpenAI)",
"anthropic_api_key": "Clau API d'Anthropic",
"anthropic_model": "Model d'Anthropic",
"anthropic_temperature": "Temperatura (Anthropic)",
"google_api_key": "Clau API de Google",
"google_model": "Model de Google",
"google_temperature": "Temperatura (Google)",
"groq_api_key": "Clau API de Groq",
"groq_model": "Model de Groq",
"groq_temperature": "Temperatura (Groq)",
"localai_ip": "Adreça IP de LocalAI",
"localai_port": "Port de LocalAI",
"localai_https": "Utilitzar HTTPS per a LocalAI",
"localai_model": "Model de LocalAI",
"localai_temperature": "Temperatura (LocalAI)",
"ollama_ip": "Adreça IP d'Ollama",
"ollama_port": "Port d'Ollama",
"ollama_https": "Utilitzar HTTPS per a Ollama",
"ollama_model": "Model d'Ollama",
"ollama_temperature": "Temperatura (Ollama)",
"ollama_disable_think": "Desactivar Mode de Pensament (Ollama)",
"custom_openai_endpoint": "URL de l'API d'OpenAI Personalitzada",
"custom_openai_api_key": "Clau API d'OpenAI Personalitzada",
"custom_openai_model": "Model d'OpenAI Personalitzat",
"custom_openai_temperature": "Temperatura (OpenAI Personalitzat)",
"mistral_api_key": "Clau API de Mistral",
"mistral_model": "Model de Mistral",
"mistral_temperature": "Temperatura (Mistral AI)",
"perplexity_api_key": "Clau API de Perplexity",
"perplexity_model": "Model de Perplexity",
"perplexity_temperature": "Temperatura (Perplexity AI)",
"openrouter_api_key": "Clau API d'OpenRouter",
"openrouter_model": "Model d'OpenRouter",
"openrouter_reasoning_max_tokens": "Màxim de tokens de raonament d'OpenRouter",
"openrouter_temperature": "Temperatura (OpenRouter)",
"openai_azure_api_key": "Clau API d'Azure OpenAI",
"openai_azure_deployment_id": "ID de Desplegament d'Azure",
"openai_azure_endpoint": "URL de l'API d'Azure",
"openai_azure_api_version": "Versió de l'API d'Azure",
"openai_azure_temperature": "Temperatura (OpenAI Azure)",
"generic_openai_api_endpoint": "URL completa de l'API d'OpenAI genèrica (hauria d'acabar amb 'completions')",
"generic_openai_api_key": "Clau API d'OpenAI Genèric",
"generic_openai_model": "Model d'OpenAI Genèric",
"generic_openai_temperature": "Temperatura (OpenAI Genèric)",
"generic_openai_validation_endpoint": "URL de validació (OpenAI Genèric, hauria d'acabar amb 'models')",
"generic_openai_enable_validation": "Activar Validació (OpenAI Genèric)",
"max_input_tokens": "Màxim de tokens d'entrada",
"max_output_tokens": "Màxim de tokens de sortida"
},
"description": "Ajusta la configuració dels teus proveïdors d'IA. S'utilitzaran els camps rellevants per al proveïdor que hagis configurat. Els paràmetres comuns, com els límits de tokens, es configuren per a cada proveïdor."
}
},
"error": {
"invalid_input": "Un o més valors són invàlids."
}
},
"services": {
"generate_suggestions": {
"name": "Generar Suggeriments",
"description": "Activar manualment els suggeriments d'automatització amb IA.",
"fields": {
"provider_config": {
"name": "Configuració del Proveïdor",
"description": "Quina configuració de proveïdor utilitzar (si en tens diverses)?"
},
"custom_prompt": {
"name": "Prompt Personalitzat",
"description": "Prompt personalitzat opcional per anul·lar el prompt del sistema per defecte o guiar els suggeriments cap a temes específics."
},
"all_entities": {
"name": "Considerar Totes les Entitats",
"description": "Si és cert, considerar totes les entitats en lloc de només les noves."
},
"domains": {
"name": "Dominis",
"description": "Llista de dominis a considerar. Si està buit, es consideren tots els dominis."
},
"entity_limit": {
"name": "Límit d'Entitats",
"description": "Nombre màxim d'entitats a considerar (seleccionades aleatòriament)."
},
"automation_read_yaml": {
"name": "Llegir el fitxer 'automations.yaml'",
"description": "Llegeix i adjunta el codi YAML de les automatitzacions trobades al fitxer 'automations.yaml'. Aquesta acció consumirà molts tokens d'entrada, fes-la servir amb precaució i amb models que tinguin una finestra de context gran (p. ex., Gemini)."
},
"automation_limit": {
"name": "Límit d'Automatitzacions",
"description": "Nombre màxim d'automatitzacions a analitzar (per defecte: 100)."
}
}
}
}
}
@@ -0,0 +1,293 @@
{
"title": "AI Návrhář automatizací",
"config": {
"step": {
"user": {
"title": "Vyberte poskytovatele AI",
"data": {
"provider": "Poskytovatel AI"
}
},
"openai": {
"title": "Nakonfigurovat OpenAI",
"data": {
"openai_api_key": "OpenAI API klíč",
"openai_model": "Model OpenAI",
"openai_temperature": "Teplota",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
},
"anthropic": {
"title": "Nakonfigurovat Anthropic",
"data": {
"anthropic_api_key": "Anthropic API klíč",
"anthropic_model": "Model Anthropic",
"anthropic_temperature": "Teplota",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
},
"google": {
"title": "Nakonfigurovat Google",
"data": {
"google_api_key": "Google API klíč",
"google_model": "Model Google",
"google_temperature": "Teplota",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
},
"groq": {
"title": "Nakonfigurovat Groq",
"data": {
"groq_api_key": "Groq API klíč",
"groq_model": "Model Groq",
"groq_temperature": "Teplota",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
},
"localai": {
"title": "Nakonfigurovat LocalAI",
"data": {
"localai_ip": "IP adresa LocalAI",
"localai_port": "Port LocalAI",
"localai_https": "Použít HTTPS pro LocalAI",
"localai_model": "Model LocalAI",
"localai_temperature": "Teplota",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
},
"ollama": {
"title": "Nakonfigurovat Ollama",
"data": {
"ollama_base_url": "Základní URL Ollama/Open WebUI",
"ollama_api_key": "API klíč Ollama/Open WebUI (volitelné)",
"ollama_ip": "IP adresa Ollama",
"ollama_port": "Port Ollama",
"ollama_https": "Použít HTTPS pro Ollama",
"ollama_model": "Model Ollama",
"ollama_temperature": "Teplota",
"ollama_disable_think": "Zakázat režim přemýšlení (Ollama)",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
},
"custom_openai": {
"title": "Nakonfigurovat vlastní OpenAI",
"data": {
"custom_openai_endpoint": "Koncový bod vlastního OpenAI",
"custom_openai_api_key": "API klíč vlastního OpenAI (volitelné)",
"custom_openai_model": "Model vlastního OpenAI",
"custom_openai_temperature": "Teplota",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
},
"mistral": {
"title": "Nakonfigurovat Mistral AI",
"data": {
"mistral_api_key": "Mistral API klíč",
"mistral_model": "Model Mistral",
"mistral_temperature": "Teplota",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
},
"perplexity": {
"title": "Nakonfigurovat Perplexity AI",
"data": {
"perplexity_api_key": "Perplexity API klíč",
"perplexity_model": "Model Perplexity",
"perplexity_temperature": "Teplota",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
},
"openrouter": {
"title": "Nakonfigurovat OpenRouter",
"data": {
"openrouter_api_key": "OpenRouter API klíč",
"openrouter_model": "Model OpenRouter",
"openrouter_reasoning_max_tokens": "Maximální počet tokenů pro uvažování OpenRouter",
"openrouter_temperature": "Teplota",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
},
"openai_azure": {
"title": "Nakonfigurovat Azure OpenAI",
"data": {
"openai_azure_api_key": "Azure OpenAI API klíč",
"openai_azure_deployment_id": "ID nasazení Azure",
"openai_azure_endpoint": "Koncový bod Azure",
"openai_azure_api_version": "Verze Azure API",
"openai_azure_temperature": "Teplota",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
},
"generic_openai": {
"title": "Nakonfigurovat obecné OpenAI",
"data": {
"generic_openai_api_endpoint": "Úplná URL adresa API obecného OpenAI (musí končit na 'completions')",
"generic_openai_api_key": "API klíč obecného OpenAI (volitelné)",
"generic_openai_model": "Model obecného OpenAI",
"generic_openai_temperature": "Teplota",
"generic_openai_validation_endpoint": "Ověřovací URL (musí končit na 'models')",
"generic_openai_enable_validation": "Povolit ověřování",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů"
}
}
},
"error": {
"api_error": "Ověření API se nezdařilo: {error_message}",
"cannot_connect": "Nepodařilo se připojit. Zkontrolujte nastavení a síť.",
"unknown": "Došlo k neznámé chybě. Podrobnosti najdete v logu."
},
"abort": {
"already_configured": "Tento poskytovatel AI je již nakonfigurován. Můžete jej upravit na stránce integrací."
}
},
"options": {
"step": {
"init": {
"title": "Nastavení AI Návrháře automatizací",
"data": {
"openai_api_key": "OpenAI API klíč",
"openai_model": "Model OpenAI",
"openai_temperature": "Teplota (OpenAI)",
"anthropic_api_key": "Anthropic API klíč",
"anthropic_model": "Model Anthropic",
"anthropic_temperature": "Teplota (Anthropic)",
"google_api_key": "Google API klíč",
"google_model": "Model Google",
"google_temperature": "Teplota (Google)",
"groq_api_key": "Groq API klíč",
"groq_model": "Model Groq",
"groq_temperature": "Teplota (Groq)",
"localai_ip": "IP adresa LocalAI",
"localai_port": "Port LocalAI",
"localai_https": "Používat HTTPS pro LocalAI",
"localai_model": "Model LocalAI",
"localai_temperature": "Teplota (LocalAI)",
"ollama_base_url": "Základní URL Ollama/Open WebUI",
"ollama_api_key": "API klíč Ollama/Open WebUI (volitelné)",
"ollama_ip": "IP adresa Ollama",
"ollama_port": "Port Ollama",
"ollama_https": "Používat HTTPS pro Ollama",
"ollama_model": "Model Ollama",
"ollama_temperature": "Teplota (Ollama)",
"ollama_disable_think": "Zakázat režim přemýšlení (Ollama)",
"custom_openai_endpoint": "Koncový bod vlastního OpenAI",
"custom_openai_api_key": "API klíč vlastního OpenAI",
"custom_openai_model": "Model vlastního OpenAI",
"custom_openai_temperature": "Teplota (vlastní OpenAI)",
"mistral_api_key": "Mistral API klíč",
"mistral_model": "Model Mistral",
"mistral_temperature": "Teplota (Mistral AI)",
"perplexity_api_key": "Perplexity API klíč",
"perplexity_model": "Model Perplexity",
"perplexity_temperature": "Teplota (Perplexity AI)",
"openrouter_api_key": "OpenRouter API klíč",
"openrouter_model": "Model OpenRouter",
"openrouter_reasoning_max_tokens": "Maximální počet tokenů pro uvažování OpenRouter",
"openrouter_temperature": "Teplota (OpenRouter)",
"openai_azure_api_key": "Azure OpenAI API klíč",
"openai_azure_deployment_id": "ID nasazení Azure",
"openai_azure_endpoint": "Koncový bod Azure",
"openai_azure_api_version": "Verze Azure API",
"openai_azure_temperature": "Teplota (Azure OpenAI)",
"generic_openai_api_endpoint": "Úplná URL adresa API obecného OpenAI (musí končit na 'completions')",
"generic_openai_api_key": "API klíč obecného OpenAI",
"generic_openai_model": "Model obecného OpenAI",
"generic_openai_temperature": "Teplota (obecné OpenAI)",
"generic_openai_validation_endpoint": "Ověřovací URL (obecné OpenAI, musí končit na 'models')",
"generic_openai_enable_validation": "Povolit ověřování (obecné OpenAI)",
"max_input_tokens": "Maximální počet vstupních tokenů",
"max_output_tokens": "Maximální počet výstupních tokenů",
"custom_system_prompt": "Trvalý vlastní systémový prompt",
"excluded_domains": "Vyloučené domény",
"excluded_entities": "Vyloučené entity",
"excluded_areas": "Vyloučené oblasti",
"history_retention": "Uchování historie návrhů",
"request_timeout": "Časový limit požadavku na poskytovatele",
"openai_reasoning_effort": "Úroveň uvažování OpenAI"
},
"description": "Upravte nastavení svých poskytovatelů AI. Použijí se pole relevantní pro nakonfigurovaného poskytovatele. Běžná nastavení, jako jsou limity tokenů, se konfigurují pro každého poskytovatele zvlášť."
}
},
"error": {
"invalid_input": "Jedna nebo více hodnot je neplatná."
}
},
"services": {
"generate_suggestions": {
"name": "Generovat návrhy",
"description": "Ručně spustit návrhy automatizací AI.",
"fields": {
"provider_config": {
"name": "Konfigurace poskytovatele",
"description": "Kterou konfiguraci poskytovatele použít (pokud jich máte více)?"
},
"custom_prompt": {
"name": "Vlastní prompt",
"description": "Volitelný vlastní prompt pro přepsání výchozího systémového promptu nebo směrování návrhů k určitým tématům."
},
"all_entities": {
"name": "Zohlednit všechny entity",
"description": "Pokud je zapnuto, zohlední všechny entity místo pouze nových entit."
},
"domains": {
"name": "Domény",
"description": "Seznam domén ke zohlednění. Pokud je prázdný, zohlední všechny domény."
},
"entity_limit": {
"name": "Limit entit",
"description": "Maximální počet entit ke zohlednění (náhodně vybrané)."
},
"automation_read_yaml": {
"name": "Číst soubor 'automations.yaml'",
"description": "Přečte a přidá YAML kód automatizací nalezených v souboru 'automations.yaml'. Tato akce použije mnoho vstupních tokenů, používejte ji opatrně a s modely s velkým kontextovým oknem (např. Gemini)."
},
"automation_limit": {
"name": "Limit automatizací",
"description": "Maximální počet automatizací k analýze (výchozí: 100)."
},
"exclude_domains": {
"name": "Vyloučit domény",
"description": "Domény, které se mají vyloučit z analýzy."
},
"exclude_entities": {
"name": "Vyloučit entity",
"description": "ID entit, které se mají vyloučit z analýzy."
},
"exclude_areas": {
"name": "Vyloučit oblasti",
"description": "Oblasti, které se mají vyloučit z analýzy."
}
}
},
"clear_history": {
"name": "Vymazat historii návrhů",
"description": "Vymazat všechny uložené návrhy automatizací AI."
},
"update_suggestion": {
"name": "Aktualizovat návrh",
"description": "Aktualizovat stav kontroly uloženého návrhu automatizace AI.",
"fields": {
"suggestion_id": {
"name": "ID návrhu",
"description": "ID návrhu, který se má aktualizovat."
},
"status": {
"name": "Stav",
"description": "Nový stav kontroly."
}
}
}
}
}
@@ -0,0 +1,252 @@
{
"title": "AI Automation Suggester",
"config": {
"step": {
"user": {
"title": "KI-Anbieter auswählen",
"data": {
"provider": "KI-Anbieter"
}
},
"openai": {
"title": "OpenAI konfigurieren",
"data": {
"openai_api_key": "OpenAI API-Schlüssel",
"openai_model": "OpenAI-Modell",
"openai_temperature": "Temperatur",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
},
"anthropic": {
"title": "Anthropic konfigurieren",
"data": {
"anthropic_api_key": "Anthropic API-Schlüssel",
"anthropic_model": "Anthropic-Modell",
"anthropic_temperature": "Temperatur",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
},
"google": {
"title": "Google konfigurieren",
"data": {
"google_api_key": "Google API-Schlüssel",
"google_model": "Google-Modell",
"google_temperature": "Temperatur",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
},
"groq": {
"title": "Groq konfigurieren",
"data": {
"groq_api_key": "Groq API-Schlüssel",
"groq_model": "Groq-Modell",
"groq_temperature": "Temperatur",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
},
"localai": {
"title": "LocalAI konfigurieren",
"data": {
"localai_ip": "LocalAI IP-Adresse",
"localai_port": "LocalAI Port",
"localai_https": "HTTPS für LocalAI verwenden",
"localai_model": "LocalAI-Modell",
"localai_temperature": "Temperatur",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
},
"ollama": {
"title": "Ollama konfigurieren",
"data": {
"ollama_ip": "Ollama IP-Adresse",
"ollama_port": "Ollama Port",
"ollama_https": "HTTPS für Ollama verwenden",
"ollama_model": "Ollama-Modell",
"ollama_temperature": "Temperatur",
"ollama_disable_think": "Denkmodus deaktivieren (Ollama)",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
},
"custom_openai": {
"title": "Benutzerdefiniertes OpenAI konfigurieren",
"data": {
"custom_openai_endpoint": "Benutzerdefinierter OpenAI-Endpunkt",
"custom_openai_api_key": "Benutzerdefinierter OpenAI API-Schlüssel (Optional)",
"custom_openai_model": "Benutzerdefiniertes OpenAI-Modell",
"custom_openai_temperature": "Temperatur",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
},
"mistral": {
"title": "Mistral AI konfigurieren",
"data": {
"mistral_api_key": "Mistral API-Schlüssel",
"mistral_model": "Mistral-Modell",
"mistral_temperature": "Temperatur",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
},
"perplexity": {
"title": "Perplexity AI konfigurieren",
"data": {
"perplexity_api_key": "Perplexity API-Schlüssel",
"perplexity_model": "Perplexity-Modell",
"perplexity_temperature": "Temperatur",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
},
"openrouter": {
"title": "OpenRouter konfigurieren",
"data": {
"openrouter_api_key": "OpenRouter API-Schlüssel",
"openrouter_model": "OpenRouter-Modell",
"openrouter_reasoning_max_tokens": "Maximale Tokens für Schlussfolgerungen (OpenRouter)",
"openrouter_temperature": "Temperatur",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
},
"openai_azure": {
"title": "OpenAI Azure konfigurieren",
"data": {
"openai_azure_api_key": "Azure OpenAI API-Schlüssel",
"openai_azure_deployment_id": "Azure Bereitstellungs-ID",
"openai_azure_endpoint": "Azure-Endpunkt",
"openai_azure_api_version": "Azure API-Version",
"openai_azure_temperature": "Temperatur",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
},
"generic_openai": {
"title": "Generisches OpenAI konfigurieren",
"data": {
"generic_openai_api_endpoint": "Vollständige API-URL für generisches OpenAI (sollte auf 'completions' enden)",
"generic_openai_api_key": "Generischer OpenAI API-Schlüssel (optional)",
"generic_openai_model": "Generisches OpenAI-Modell",
"generic_openai_temperature": "Temperatur",
"generic_openai_validation_endpoint": "Validierungs-URL (sollte auf 'models' enden)",
"generic_openai_enable_validation": "Validierung aktivieren",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
}
}
},
"error": {
"api_error": "API-Validierung fehlgeschlagen: {error_message}",
"cannot_connect": "Verbindung fehlgeschlagen. Bitte überprüfen Sie Ihre Einstellungen und Ihr Netzwerk.",
"unknown": "Ein unbekannter Fehler ist aufgetreten. Bitte überprüfen Sie die Protokolle für Details."
},
"abort": {
"already_configured": "Dieser KI-Anbieter ist bereits konfiguriert. Sie können ihn auf der Integrationsseite bearbeiten."
}
},
"options": {
"step": {
"init": {
"title": "Optionen für KI-Automatisierungsvorschläge",
"data": {
"openai_api_key": "OpenAI API-Schlüssel",
"openai_model": "OpenAI-Modell",
"openai_temperature": "Temperatur (OpenAI)",
"anthropic_api_key": "Anthropic API-Schlüssel",
"anthropic_model": "Anthropic-Modell",
"anthropic_temperature": "Temperatur (Anthropic)",
"google_api_key": "Google API-Schlüssel",
"google_model": "Google-Modell",
"google_temperature": "Temperatur (Google)",
"groq_api_key": "Groq API-Schlüssel",
"groq_model": "Groq-Modell",
"groq_temperature": "Temperatur (Groq)",
"localai_ip": "LocalAI IP-Adresse",
"localai_port": "LocalAI Port",
"localai_https": "HTTPS für LocalAI verwenden",
"localai_model": "LocalAI-Modell",
"localai_temperature": "Temperatur (LocalAI)",
"ollama_ip": "Ollama IP-Adresse",
"ollama_port": "Ollama Port",
"ollama_https": "HTTPS für Ollama verwenden",
"ollama_model": "Ollama-Modell",
"ollama_temperature": "Temperatur (Ollama)",
"ollama_disable_think": "Denkmodus deaktivieren (Ollama)",
"custom_openai_endpoint": "Benutzerdefinierter OpenAI-Endpunkt",
"custom_openai_api_key": "Benutzerdefinierter OpenAI API-Schlüssel",
"custom_openai_model": "Benutzerdefiniertes OpenAI-Modell",
"custom_openai_temperature": "Temperatur (Benutzerdefiniertes OpenAI)",
"mistral_api_key": "Mistral API-Schlüssel",
"mistral_model": "Mistral-Modell",
"mistral_temperature": "Temperatur (Mistral AI)",
"perplexity_api_key": "Perplexity API-Schlüssel",
"perplexity_model": "Perplexity-Modell",
"perplexity_temperature": "Temperatur (Perplexity AI)",
"openrouter_api_key": "OpenRouter API-Schlüssel",
"openrouter_model": "OpenRouter-Modell",
"openrouter_reasoning_max_tokens": "Maximale Tokens für Schlussfolgerungen (OpenRouter)",
"openrouter_temperature": "Temperatur (OpenRouter)",
"openai_azure_api_key": "Azure OpenAI API-Schlüssel",
"openai_azure_deployment_id": "Azure Bereitstellungs-ID",
"openai_azure_endpoint": "Azure-Endpunkt",
"openai_azure_api_version": "Azure API-Version",
"openai_azure_temperature": "Temperatur (OpenAI Azure)",
"generic_openai_api_endpoint": "Vollständige API-URL für generisches OpenAI (sollte auf 'completions' enden)",
"generic_openai_api_key": "Generischer OpenAI API-Schlüssel",
"generic_openai_model": "Generisches OpenAI-Modell",
"generic_openai_temperature": "Temperatur (Generisches OpenAI)",
"generic_openai_validation_endpoint": "Validierungs-URL (Generisches OpenAI, sollte auf 'models' enden)",
"generic_openai_enable_validation": "Validierung aktivieren (Generisches OpenAI)",
"max_input_tokens": "Maximale Eingabe-Tokens",
"max_output_tokens": "Maximale Ausgabe-Tokens"
},
"description": "Passen Sie die Einstellungen für Ihre KI-Anbieter an. Es werden nur die für den konfigurierten Anbieter relevanten Felder verwendet. Allgemeine Einstellungen wie Token-Limits werden pro Anbieter festgelegt."
}
},
"error": {
"invalid_input": "Ein oder mehrere Werte sind ungültig."
}
},
"services": {
"generate_suggestions": {
"name": "Vorschläge generieren",
"description": "Manuelles Auslösen von KI-Automatisierungsvorschlägen.",
"fields": {
"provider_config": {
"name": "Anbieterkonfiguration",
"description": "Welche Anbieterkonfiguration soll verwendet werden (falls mehrere vorhanden)?"
},
"custom_prompt": {
"name": "Benutzerdefinierter Prompt",
"description": "Optionaler benutzerdefinierter Prompt, um den Standard-System-Prompt zu überschreiben oder die Vorschläge auf bestimmte Themen auszurichten."
},
"all_entities": {
"name": "Alle Entitäten berücksichtigen",
"description": "Wenn wahr, werden alle Entitäten berücksichtigt, anstatt nur neue."
},
"domains": {
"name": "Domänen",
"description": "Liste der zu berücksichtigenden Domänen. Wenn leer, werden alle Domänen berücksichtigt."
},
"entity_limit": {
"name": "Entitäten-Limit",
"description": "Maximale Anzahl von Entitäten, die berücksichtigt werden (zufällig ausgewählt)."
},
"automation_read_yaml": {
"name": "Datei 'automations.yaml' lesen",
"description": "Liest den YAML-Code der in der 'automations.yaml'-Datei gefundenen Automatisierungen und fügt ihn an. Diese Aktion verbraucht viele Eingabe-Tokens. Verwenden Sie sie mit Vorsicht und mit Modellen, die ein großes Kontextfenster haben (z.B. Gemini)."
},
"automation_limit": {
"name": "Limit für Automatisierungen",
"description": "Maximale Anzahl der zu analysierenden Automatisierungen (Standard: 100)."
}
}
}
}
}
@@ -0,0 +1,308 @@
{
"title": "AI Automation Suggester",
"config": {
"step": {
"user": {
"title": "Select AI Provider",
"data": {
"provider": "AI Provider"
}
},
"openai": {
"title": "Configure OpenAI",
"data": {
"openai_api_key": "OpenAI API Key",
"openai_model": "OpenAI Model",
"openai_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"anthropic": {
"title": "Configure Anthropic",
"data": {
"anthropic_api_key": "Anthropic API Key",
"anthropic_model": "Anthropic Model",
"anthropic_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"google": {
"title": "Configure Google",
"data": {
"google_api_key": "Google API Key",
"google_model": "Google Model",
"google_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"groq": {
"title": "Configure Groq",
"data": {
"groq_api_key": "Groq API Key",
"groq_model": "Groq Model",
"groq_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"localai": {
"title": "Configure LocalAI",
"data": {
"localai_ip": "LocalAI IP Address",
"localai_port": "LocalAI Port",
"localai_https": "Use HTTPS for LocalAI",
"localai_model": "LocalAI Model",
"localai_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"ollama": {
"title": "Configure Ollama",
"data": {
"ollama_base_url": "Ollama/Open WebUI Base URL",
"ollama_api_key": "Ollama/Open WebUI API Key (optional)",
"ollama_ip": "Ollama IP Address",
"ollama_port": "Ollama Port",
"ollama_https": "Use HTTPS for Ollama",
"ollama_model": "Ollama Model",
"ollama_temperature": "Temperature",
"ollama_disable_think": "Disable Think Mode (Ollama)",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"custom_openai": {
"title": "Configure Custom OpenAI",
"data": {
"custom_openai_endpoint": "Custom OpenAI Endpoint",
"custom_openai_api_key": "Custom OpenAI API Key (Optional)",
"custom_openai_model": "Custom OpenAI Model",
"custom_openai_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"mistral": {
"title": "Configure Mistral AI",
"data": {
"mistral_api_key": "Mistral API Key",
"mistral_model": "Mistral Model",
"mistral_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"perplexity": {
"title": "Configure Perplexity AI",
"data": {
"perplexity_api_key": "Perplexity API Key",
"perplexity_model": "Perplexity Model",
"perplexity_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"openrouter": {
"title": "Configure OpenRouter",
"data": {
"openrouter_api_key": "OpenRouter API Key",
"openrouter_model": "OpenRouter Model",
"openrouter_reasoning_max_tokens": "OpenRouter Reasoning Max Tokens",
"openrouter_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"openai_azure": {
"title": "Configure OpenAI Azure",
"data": {
"openai_azure_api_key": "Azure OpenAI API Key",
"openai_azure_deployment_id": "Azure Deployment ID",
"openai_azure_endpoint": "Azure Endpoint",
"openai_azure_api_version": "Azure API Version",
"openai_azure_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"generic_openai": {
"title": "Configure Generic OpenAI",
"data": {
"generic_openai_api_endpoint": "Generic OpenAI API Full URL (should end with 'completions')",
"generic_openai_api_key": "Generic OpenAI API Key (Optional)",
"generic_openai_model": "Generic OpenAI Model",
"generic_openai_temperature": "Temperature",
"generic_openai_validation_endpoint": "Validation URL (should end with 'models')",
"generic_openai_enable_validation": "Enable Validation",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
},
"litellm": {
"title": "Configure LiteLLM",
"data": {
"litellm_model": "LiteLLM Model (e.g. openai/gpt-4o, anthropic/claude-sonnet-4-6, groq/llama-3.3-70b-versatile)",
"litellm_api_key": "API Key (optional if set via environment variable)",
"litellm_api_base": "API Base URL (optional, for LiteLLM proxy)",
"litellm_temperature": "Temperature",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens"
}
}
},
"error": {
"api_error": "API validation failed: {error_message}",
"cannot_connect": "Failed to connect. Please check your settings and network.",
"unknown": "An unknown error occurred. Please check logs for details."
},
"abort": {
"already_configured": "This AI provider is already configured. You can edit it from the integrations page."
}
},
"options": {
"step": {
"init": {
"title": "AI Automation Suggester Options",
"data": {
"openai_api_key": "OpenAI API Key",
"openai_model": "OpenAI Model",
"openai_temperature": "Temperature (OpenAI)",
"anthropic_api_key": "Anthropic API Key",
"anthropic_model": "Anthropic Model",
"anthropic_temperature": "Temperature (Anthropic)",
"google_api_key": "Google API Key",
"google_model": "Google Model",
"google_temperature": "Temperature (Google)",
"groq_api_key": "Groq API Key",
"groq_model": "Groq Model",
"groq_temperature": "Temperature (Groq)",
"localai_ip": "LocalAI IP Address",
"localai_port": "LocalAI Port",
"localai_https": "Use HTTPS for LocalAI",
"localai_model": "LocalAI Model",
"localai_temperature": "Temperature (LocalAI)",
"ollama_base_url": "Ollama/Open WebUI Base URL",
"ollama_api_key": "Ollama/Open WebUI API Key (optional)",
"ollama_ip": "Ollama IP Address",
"ollama_port": "Ollama Port",
"ollama_https": "Use HTTPS for Ollama",
"ollama_model": "Ollama Model",
"ollama_temperature": "Temperature (Ollama)",
"ollama_disable_think": "Disable Think Mode (Ollama)",
"custom_openai_endpoint": "Custom OpenAI Endpoint",
"custom_openai_api_key": "Custom OpenAI API Key",
"custom_openai_model": "Custom OpenAI Model",
"custom_openai_temperature": "Temperature (Custom OpenAI)",
"mistral_api_key": "Mistral API Key",
"mistral_model": "Mistral Model",
"mistral_temperature": "Temperature (Mistral AI)",
"perplexity_api_key": "Perplexity API Key",
"perplexity_model": "Perplexity Model",
"perplexity_temperature": "Temperature (Perplexity AI)",
"openrouter_api_key": "OpenRouter API Key",
"openrouter_model": "OpenRouter Model",
"openrouter_reasoning_max_tokens": "OpenRouter Reasoning Max Tokens",
"openrouter_temperature": "Temperature (OpenRouter)",
"openai_azure_api_key": "Azure OpenAI API Key",
"openai_azure_deployment_id": "Azure Deployment ID",
"openai_azure_endpoint": "Azure Endpoint",
"openai_azure_api_version": "Azure API Version",
"openai_azure_temperature": "Temperature (OpenAI Azure)",
"generic_openai_api_endpoint": "Generic OpenAI API Full URL (should end with 'completions')",
"generic_openai_api_key": "Generic OpenAI API Key",
"generic_openai_model": "Generic OpenAI Model",
"generic_openai_temperature": "Temperature (Generic OpenAI)",
"generic_openai_validation_endpoint": "Validation URL (Generic OpenAI, should end with 'models')",
"generic_openai_enable_validation": "Enable Validation (Generic OpenAI)",
"litellm_api_key": "LiteLLM API Key",
"litellm_model": "LiteLLM Model",
"litellm_api_base": "LiteLLM API Base URL",
"litellm_temperature": "Temperature (LiteLLM)",
"max_input_tokens": "Max Input Tokens",
"max_output_tokens": "Max Output Tokens",
"custom_system_prompt": "Persistent Custom System Prompt",
"excluded_domains": "Excluded Domains",
"excluded_entities": "Excluded Entities",
"excluded_areas": "Excluded Areas",
"history_retention": "Suggestion History Retention",
"request_timeout": "Provider Request Timeout",
"openai_reasoning_effort": "OpenAI Reasoning Effort"
},
"description": "Adjust settings for your AI providers. Fields relevant to your configured provider will be used. Common settings like token limits are configured per provider."
}
},
"error": {
"invalid_input": "One or more values are invalid."
}
},
"services": {
"generate_suggestions": {
"name": "Generate Suggestions",
"description": "Manually trigger AI automation suggestions.",
"fields": {
"provider_config": {
"name": "Provider Configuration",
"description": "Which provider configuration to use (if you have multiple)"
},
"custom_prompt": {
"name": "Custom Prompt",
"description": "Optional custom prompt to override the default system prompt or guide the suggestions towards specific themes"
},
"all_entities": {
"name": "Consider All Entities",
"description": "If true, consider all entities instead of just new entities."
},
"domains": {
"name": "Domains",
"description": "List of domains to consider. If empty, consider all domains."
},
"entity_limit": {
"name": "Entity Limit",
"description": "Maximum number of entities to consider (randomly selected)."
},
"automation_read_yaml": {
"name": "Read 'automations.yaml' file",
"description": "Reads and appends the YAML code of the automations found in the 'automations.yaml' file. This action will use a lot of input tokens, use it with care and with models with a large context window (e.g. Gemini)."
},
"automation_limit": {
"name": "Automation Limit",
"description": "Maximum number of automations to analyze (default: 100)."
},
"exclude_domains": {
"name": "Exclude Domains",
"description": "Domains to exclude from analysis."
},
"exclude_entities": {
"name": "Exclude Entities",
"description": "Entity IDs to exclude from analysis."
},
"exclude_areas": {
"name": "Exclude Areas",
"description": "Areas to exclude from analysis."
}
}
},
"clear_history": {
"name": "Clear Suggestion History",
"description": "Clear all stored AI automation suggestions."
},
"update_suggestion": {
"name": "Update Suggestion",
"description": "Update the review status for a stored AI automation suggestion.",
"fields": {
"suggestion_id": {
"name": "Suggestion ID",
"description": "The suggestion ID to update."
},
"status": {
"name": "Status",
"description": "The new review status."
}
}
}
}
}
@@ -0,0 +1,252 @@
{
"title": "AI Automation Suggester",
"config": {
"step": {
"user": {
"title": "Seleccionar Proveedor de IA",
"data": {
"provider": "Proveedor de IA"
}
},
"openai": {
"title": "Configurar OpenAI",
"data": {
"openai_api_key": "Clave API de OpenAI",
"openai_model": "Modelo de OpenAI",
"openai_temperature": "Temperatura",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
},
"anthropic": {
"title": "Configurar Anthropic",
"data": {
"anthropic_api_key": "Clave API de Anthropic",
"anthropic_model": "Modelo de Anthropic",
"anthropic_temperature": "Temperatura",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
},
"google": {
"title": "Configurar Google",
"data": {
"google_api_key": "Clave API de Google",
"google_model": "Modelo de Google",
"google_temperature": "Temperatura",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
},
"groq": {
"title": "Configurar Groq",
"data": {
"groq_api_key": "Clave API de Groq",
"groq_model": "Modelo de Groq",
"groq_temperature": "Temperatura",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
},
"localai": {
"title": "Configurar LocalAI",
"data": {
"localai_ip": "Dirección IP de LocalAI",
"localai_port": "Puerto de LocalAI",
"localai_https": "Usar HTTPS para LocalAI",
"localai_model": "Modelo de LocalAI",
"localai_temperature": "Temperatura",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
},
"ollama": {
"title": "Configurar Ollama",
"data": {
"ollama_ip": "Dirección IP de Ollama",
"ollama_port": "Puerto de Ollama",
"ollama_https": "Usar HTTPS para Ollama",
"ollama_model": "Modelo de Ollama",
"ollama_temperature": "Temperatura",
"ollama_disable_think": "Desactivar Modo de Pensamiento (Ollama)",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
},
"custom_openai": {
"title": "Configurar OpenAI Personalizado",
"data": {
"custom_openai_endpoint": "URL de la API de OpenAI Personalizada",
"custom_openai_api_key": "Clave API de OpenAI Personalizada (Opcional)",
"custom_openai_model": "Modelo de OpenAI Personalizado",
"custom_openai_temperature": "Temperatura",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
},
"mistral": {
"title": "Configurar Mistral AI",
"data": {
"mistral_api_key": "Clave API de Mistral",
"mistral_model": "Modelo de Mistral",
"mistral_temperature": "Temperatura",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
},
"perplexity": {
"title": "Configurar Perplexity AI",
"data": {
"perplexity_api_key": "Clave API de Perplexity",
"perplexity_model": "Modelo de Perplexity",
"perplexity_temperature": "Temperatura",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
},
"openrouter": {
"title": "Configurar OpenRouter",
"data": {
"openrouter_api_key": "Clave API de OpenRouter",
"openrouter_model": "Modelo de OpenRouter",
"openrouter_reasoning_max_tokens": "Máximo de tokens de razonamiento de OpenRouter",
"openrouter_temperature": "Temperatura",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
},
"openai_azure": {
"title": "Configurar OpenAI Azure",
"data": {
"openai_azure_api_key": "Clave API de Azure OpenAI",
"openai_azure_deployment_id": "ID de Despliegue de Azure",
"openai_azure_endpoint": "URL de la API de Azure",
"openai_azure_api_version": "Versión de la API de Azure",
"openai_azure_temperature": "Temperatura",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
},
"generic_openai": {
"title": "Configurar OpenAI Genérico",
"data": {
"generic_openai_api_endpoint": "URL completa de la API de OpenAI genérica (debe terminar en 'completions')",
"generic_openai_api_key": "Clave API de OpenAI Genérico (Opcional)",
"generic_openai_model": "Modelo de OpenAI Genérico",
"generic_openai_temperature": "Temperatura",
"generic_openai_validation_endpoint": "URL de validación (debe terminar en 'models')",
"generic_openai_enable_validation": "Habilitar Validación",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
}
}
},
"error": {
"api_error": "Falló la validación de la API: {error_message}",
"cannot_connect": "No se pudo conectar. Por favor, revisa tu configuración y red.",
"unknown": "Ocurrió un error desconocido. Por favor, revisa los registros para más detalles."
},
"abort": {
"already_configured": "Este proveedor de IA ya está configurado. Puedes editarlo desde la página de integraciones."
}
},
"options": {
"step": {
"init": {
"title": "Opciones del Sugeridor de Automatizaciones con IA",
"data": {
"openai_api_key": "Clave API de OpenAI",
"openai_model": "Modelo de OpenAI",
"openai_temperature": "Temperatura (OpenAI)",
"anthropic_api_key": "Clave API de Anthropic",
"anthropic_model": "Modelo de Anthropic",
"anthropic_temperature": "Temperatura (Anthropic)",
"google_api_key": "Clave API de Google",
"google_model": "Modelo de Google",
"google_temperature": "Temperatura (Google)",
"groq_api_key": "Clave API de Groq",
"groq_model": "Modelo de Groq",
"groq_temperature": "Temperatura (Groq)",
"localai_ip": "Dirección IP de LocalAI",
"localai_port": "Puerto de LocalAI",
"localai_https": "Usar HTTPS para LocalAI",
"localai_model": "Modelo de LocalAI",
"localai_temperature": "Temperatura (LocalAI)",
"ollama_ip": "Dirección IP de Ollama",
"ollama_port": "Puerto de Ollama",
"ollama_https": "Usar HTTPS para Ollama",
"ollama_model": "Modelo de Ollama",
"ollama_temperature": "Temperatura (Ollama)",
"ollama_disable_think": "Desactivar Modo de Pensamiento (Ollama)",
"custom_openai_endpoint": "URL de la API de OpenAI Personalizada",
"custom_openai_api_key": "Clave API de OpenAI Personalizada",
"custom_openai_model": "Modelo de OpenAI Personalizado",
"custom_openai_temperature": "Temperatura (OpenAI Personalizado)",
"mistral_api_key": "Clave API de Mistral",
"mistral_model": "Modelo de Mistral",
"mistral_temperature": "Temperatura (Mistral AI)",
"perplexity_api_key": "Clave API de Perplexity",
"perplexity_model": "Modelo de Perplexity",
"perplexity_temperature": "Temperatura (Perplexity AI)",
"openrouter_api_key": "Clave API de OpenRouter",
"openrouter_model": "Modelo de OpenRouter",
"openrouter_reasoning_max_tokens": "Máximo de tokens de razonamiento de OpenRouter",
"openrouter_temperature": "Temperatura (OpenRouter)",
"openai_azure_api_key": "Clave API de Azure OpenAI",
"openai_azure_deployment_id": "ID de Despliegue de Azure",
"openai_azure_endpoint": "URL de la API de Azure",
"openai_azure_api_version": "Versión de la API de Azure",
"openai_azure_temperature": "Temperatura (OpenAI Azure)",
"generic_openai_api_endpoint": "URL completa de la API de OpenAI genérica (debe terminar en 'completions')",
"generic_openai_api_key": "Clave API de OpenAI Genérico",
"generic_openai_model": "Modelo de OpenAI Genérico",
"generic_openai_temperature": "Temperatura (OpenAI Genérico)",
"generic_openai_validation_endpoint": "URL de validación (OpenAI Genérico, debe terminar en 'models')",
"generic_openai_enable_validation": "Habilitar Validación (OpenAI Genérico)",
"max_input_tokens": "Máximo de tokens de entrada",
"max_output_tokens": "Máximo de tokens de salida"
},
"description": "Ajusta la configuración de tus proveedores de IA. Se utilizarán los campos relevantes para el proveedor que hayas configurado. Los ajustes comunes, como los límites de tokens, se configuran para cada proveedor."
}
},
"error": {
"invalid_input": "Uno o más valores son inválidos."
}
},
"services": {
"generate_suggestions": {
"name": "Generar Sugerencias",
"description": "Activar manualmente las sugerencias de automatización con IA.",
"fields": {
"provider_config": {
"name": "Configuración del Proveedor",
"description": "¿Qué configuración de proveedor usar (si tienes varias)?"
},
"custom_prompt": {
"name": "Prompt Personalizado",
"description": "Prompt personalizado opcional para anular el prompt del sistema predeterminado o guiar las sugerencias hacia temas específicos."
},
"all_entities": {
"name": "Considerar Todas las Entidades",
"description": "Si es verdadero, considerar todas las entidades en lugar de solo las nuevas."
},
"domains": {
"name": "Dominios",
"description": "Lista de dominios a considerar. Si está vacío, se consideran todos los dominios."
},
"entity_limit": {
"name": "Límite de Entidades",
"description": "Número máximo de entidades a considerar (seleccionadas aleatoriamente)."
},
"automation_read_yaml": {
"name": "Leer archivo 'automations.yaml'",
"description": "Lee y adjunta el código YAML de las automatizaciones encontradas en el archivo 'automations.yaml'. Esta acción consumirá muchos tokens de entrada, úsala con precaución y con modelos que tengan una ventana de contexto grande (p. ej., Gemini)."
},
"automation_limit": {
"name": "Límite de Automatizaciones",
"description": "Número máximo de automatizaciones a analizar (por defecto: 100)."
}
}
}
}
}
@@ -0,0 +1,293 @@
{
"title": "AI Automation Suggester",
"config": {
"step": {
"user": {
"title": "Seleziona Provider AI",
"data": {
"provider": "Provider AI"
}
},
"openai": {
"title": "Configura OpenAI",
"data": {
"openai_api_key": "Chiave API OpenAI",
"openai_model": "Modello OpenAI",
"openai_temperature": "Temperatura",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
},
"anthropic": {
"title": "Configura Anthropic",
"data": {
"anthropic_api_key": "Chiave API Anthropic",
"anthropic_model": "Modello Anthropic",
"anthropic_temperature": "Temperatura",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
},
"google": {
"title": "Configura Google",
"data": {
"google_api_key": "Chiave API Google",
"google_model": "Modello Google",
"google_temperature": "Temperatura",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
},
"groq": {
"title": "Configura Groq",
"data": {
"groq_api_key": "Chiave API Groq",
"groq_model": "Modello Groq",
"groq_temperature": "Temperatura",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
},
"localai": {
"title": "Configura LocalAI",
"data": {
"localai_ip": "Indirizzo IP LocalAI",
"localai_port": "Porta LocalAI",
"localai_https": "Usa HTTPS per LocalAI",
"localai_model": "Modello LocalAI",
"localai_temperature": "Temperatura",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
},
"ollama": {
"title": "Configura Ollama",
"data": {
"ollama_base_url": "URL base Ollama/Open WebUI",
"ollama_api_key": "Chiave API Ollama/Open WebUI (opzionale)",
"ollama_ip": "Indirizzo IP Ollama",
"ollama_port": "Porta Ollama",
"ollama_https": "Usa HTTPS per Ollama",
"ollama_model": "Modello Ollama",
"ollama_temperature": "Temperatura",
"ollama_disable_think": "Disabilita Modalità Pensiero (Ollama)",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
},
"custom_openai": {
"title": "Configura OpenAI Personalizzato",
"data": {
"custom_openai_endpoint": "Endpoint OpenAI Personalizzato",
"custom_openai_api_key": "Chiave API OpenAI Personalizzata (Opzionale)",
"custom_openai_model": "Modello OpenAI Personalizzato",
"custom_openai_temperature": "Temperatura",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
},
"mistral": {
"title": "Configura Mistral AI",
"data": {
"mistral_api_key": "Chiave API Mistral",
"mistral_model": "Modello Mistral",
"mistral_temperature": "Temperatura",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
},
"perplexity": {
"title": "Configura Perplexity AI",
"data": {
"perplexity_api_key": "Chiave API Perplexity",
"perplexity_model": "Modello Perplexity",
"perplexity_temperature": "Temperatura",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
},
"openrouter": {
"title": "Configura OpenRouter",
"data": {
"openrouter_api_key": "Chiave API OpenRouter",
"openrouter_model": "Modello OpenRouter",
"openrouter_reasoning_max_tokens": "Max Token di Ragionamento OpenRouter",
"openrouter_temperature": "Temperatura",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
},
"openai_azure": {
"title": "Configura OpenAI Azure",
"data": {
"openai_azure_api_key": "Chiave API Azure OpenAI",
"openai_azure_deployment_id": "ID Distribuzione Azure",
"openai_azure_endpoint": "Endpoint Azure",
"openai_azure_api_version": "Versione API Azure",
"openai_azure_temperature": "Temperatura",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
},
"generic_openai": {
"title": "Configura OpenAI Generico",
"data": {
"generic_openai_api_endpoint": "URL API completa di OpenAI generico (deve terminare con 'completions')",
"generic_openai_api_key": "Chiave API OpenAI Generico (Opzionale)",
"generic_openai_model": "Modello OpenAI Generico",
"generic_openai_temperature": "Temperatura",
"generic_openai_validation_endpoint": "URL di validazione (deve terminare con 'models')",
"generic_openai_enable_validation": "Abilita Validazione",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output"
}
}
},
"error": {
"api_error": "Validazione API fallita: {error_message}",
"cannot_connect": "Connessione fallita. Controlla le impostazioni e la rete.",
"unknown": "Si è verificato un errore sconosciuto. Controlla i log per i dettagli."
},
"abort": {
"already_configured": "Questo provider AI è già configurato. Puoi modificarlo dalla pagina delle integrazioni."
}
},
"options": {
"step": {
"init": {
"title": "Opzioni Suggeritore di Automazioni AI",
"data": {
"openai_api_key": "Chiave API OpenAI",
"openai_model": "Modello OpenAI",
"openai_temperature": "Temperatura (OpenAI)",
"anthropic_api_key": "Chiave API Anthropic",
"anthropic_model": "Modello Anthropic",
"anthropic_temperature": "Temperatura (Anthropic)",
"google_api_key": "Chiave API Google",
"google_model": "Modello Google",
"google_temperature": "Temperatura (Google)",
"groq_api_key": "Chiave API Groq",
"groq_model": "Modello Groq",
"groq_temperature": "Temperatura (Groq)",
"localai_ip": "Indirizzo IP LocalAI",
"localai_port": "Porta LocalAI",
"localai_https": "Usa HTTPS per LocalAI",
"localai_model": "Modello LocalAI",
"localai_temperature": "Temperatura (LocalAI)",
"ollama_base_url": "URL base Ollama/Open WebUI",
"ollama_api_key": "Chiave API Ollama/Open WebUI (opzionale)",
"ollama_ip": "Indirizzo IP Ollama",
"ollama_port": "Porta Ollama",
"ollama_https": "Usa HTTPS per Ollama",
"ollama_model": "Modello Ollama",
"ollama_temperature": "Temperatura (Ollama)",
"ollama_disable_think": "Disabilita Modalità Pensiero (Ollama)",
"custom_openai_endpoint": "Endpoint OpenAI Personalizzato",
"custom_openai_api_key": "Chiave API OpenAI Personalizzata",
"custom_openai_model": "Modello OpenAI Personalizzato",
"custom_openai_temperature": "Temperatura (OpenAI Personalizzato)",
"mistral_api_key": "Chiave API Mistral",
"mistral_model": "Modello Mistral",
"mistral_temperature": "Temperatura (Mistral AI)",
"perplexity_api_key": "Chiave API Perplexity",
"perplexity_model": "Modello Perplexity",
"perplexity_temperature": "Temperatura (Perplexity AI)",
"openrouter_api_key": "Chiave API OpenRouter",
"openrouter_model": "Modello OpenRouter",
"openrouter_reasoning_max_tokens": "Max Token di Ragionamento OpenRouter",
"openrouter_temperature": "Temperatura (OpenRouter)",
"openai_azure_api_key": "Chiave API Azure OpenAI",
"openai_azure_deployment_id": "ID Deployment Azure",
"openai_azure_endpoint": "Endpoint Azure",
"openai_azure_api_version": "Versione API Azure",
"openai_azure_temperature": "Temperatura (OpenAI Azure)",
"generic_openai_api_endpoint": "URL API completa di OpenAI generico (deve terminare con 'completions')",
"generic_openai_api_key": "Chiave API OpenAI Generico",
"generic_openai_model": "Modello OpenAI Generico",
"generic_openai_temperature": "Temperatura (OpenAI Generico)",
"generic_openai_validation_endpoint": "URL di validazione (OpenAI Generico, deve terminare con 'models')",
"generic_openai_enable_validation": "Abilita Validazione (OpenAI Generico)",
"max_input_tokens": "Numero massimo di token di input",
"max_output_tokens": "Numero massimo di token di output",
"custom_system_prompt": "Prompt di sistema personalizzato persistente",
"excluded_domains": "Domini esclusi",
"excluded_entities": "Entità escluse",
"excluded_areas": "Aree escluse",
"history_retention": "Conservazione cronologia suggerimenti",
"request_timeout": "Timeout richiesta provider",
"openai_reasoning_effort": "Intensità di ragionamento OpenAI"
},
"description": "Regola le impostazioni per i tuoi provider di IA. Verranno utilizzati i campi pertinenti al provider configurato. Le impostazioni comuni, come i limiti dei token, sono configurate per ciascun provider."
}
},
"error": {
"invalid_input": "Uno o più valori non sono validi."
}
},
"services": {
"generate_suggestions": {
"name": "Genera Suggerimenti",
"description": "Attiva manualmente i suggerimenti di automazione AI.",
"fields": {
"provider_config": {
"name": "Configurazione Provider",
"description": "Quale configurazione provider utilizzare (se ne hai più di una)?"
},
"custom_prompt": {
"name": "Prompt Personalizzato",
"description": "Prompt personalizzato opzionale per sovrascrivere il prompt di sistema predefinito o guidare i suggerimenti verso temi specifici."
},
"all_entities": {
"name": "Considera Tutte le Entità",
"description": "Se vero, considera tutte le entità invece di solo quelle nuove."
},
"domains": {
"name": "Domini",
"description": "Elenco dei domini da considerare. Se vuoto, considera tutti i domini."
},
"entity_limit": {
"name": "Limite di Entità",
"description": "Numero massimo di entità da considerare (scelte casualmente)."
},
"automation_read_yaml": {
"name": "Leggi file 'automations.yaml'",
"description": "Legge e aggiunge il codice YAML delle automazioni trovate nel file 'automations.yaml'. Questa azione utilizzerà molti token di input, usala con cautela e con modelli con un'ampia finestra di contesto (es. Gemini)."
},
"automation_limit": {
"name": "Limite Automazioni",
"description": "Numero massimo di automazioni da analizzare (predefinito: 100)."
},
"exclude_domains": {
"name": "Escludi domini",
"description": "Domini da escludere dall'analisi."
},
"exclude_entities": {
"name": "Escludi entità",
"description": "ID entità da escludere dall'analisi."
},
"exclude_areas": {
"name": "Escludi aree",
"description": "Aree da escludere dall'analisi."
}
}
},
"clear_history": {
"name": "Cancella cronologia suggerimenti",
"description": "Cancella tutti i suggerimenti di automazione AI memorizzati."
},
"update_suggestion": {
"name": "Aggiorna suggerimento",
"description": "Aggiorna lo stato di revisione di un suggerimento di automazione AI memorizzato.",
"fields": {
"suggestion_id": {
"name": "ID suggerimento",
"description": "L'ID del suggerimento da aggiornare."
},
"status": {
"name": "Stato",
"description": "Il nuovo stato di revisione."
}
}
}
}
}
@@ -0,0 +1,252 @@
{
"title": "AI Automation Suggester",
"config": {
"step": {
"user": {
"title": "Selecteer AI Provider",
"data": {
"provider": "AI Provider"
}
},
"openai": {
"title": "Configureer OpenAI",
"data": {
"openai_api_key": "OpenAI API-sleutel",
"openai_model": "OpenAI Model",
"openai_temperature": "Temperatuur",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
},
"anthropic": {
"title": "Configureer Anthropic",
"data": {
"anthropic_api_key": "Anthropic API-sleutel",
"anthropic_model": "Anthropic Model",
"anthropic_temperature": "Temperatuur",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
},
"google": {
"title": "Configureer Google",
"data": {
"google_api_key": "Google API-sleutel",
"google_model": "Google Model",
"google_temperature": "Temperatuur",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
},
"groq": {
"title": "Configureer Groq",
"data": {
"groq_api_key": "Groq API-sleutel",
"groq_model": "Groq Model",
"groq_temperature": "Temperatuur",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
},
"localai": {
"title": "Configureer LocalAI",
"data": {
"localai_ip": "LocalAI IP-adres",
"localai_port": "LocalAI Poort",
"localai_https": "Gebruik HTTPS voor LocalAI",
"localai_model": "LocalAI Model",
"localai_temperature": "Temperatuur",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
},
"ollama": {
"title": "Configureer Ollama",
"data": {
"ollama_ip": "Ollama IP-adres",
"ollama_port": "Ollama Poort",
"ollama_https": "Gebruik HTTPS voor Ollama",
"ollama_model": "Ollama Model",
"ollama_temperature": "Temperatuur",
"ollama_disable_think": "Denkmodus uitschakelen (Ollama)",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
},
"custom_openai": {
"title": "Configureer Aangepaste OpenAI",
"data": {
"custom_openai_endpoint": "Aangepast OpenAI Eindpunt",
"custom_openai_api_key": "Aangepaste OpenAI API-sleutel (Optioneel)",
"custom_openai_model": "Aangepast OpenAI Model",
"custom_openai_temperature": "Temperatuur",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
},
"mistral": {
"title": "Configureer Mistral AI",
"data": {
"mistral_api_key": "Mistral API-sleutel",
"mistral_model": "Mistral Model",
"mistral_temperature": "Temperatuur",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
},
"perplexity": {
"title": "Configureer Perplexity AI",
"data": {
"perplexity_api_key": "Perplexity API-sleutel",
"perplexity_model": "Perplexity Model",
"perplexity_temperature": "Temperatuur",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
},
"openrouter": {
"title": "Configureer OpenRouter",
"data": {
"openrouter_api_key": "OpenRouter API-sleutel",
"openrouter_model": "OpenRouter Model",
"openrouter_reasoning_max_tokens": "OpenRouter Redenering Max Tokens",
"openrouter_temperature": "Temperatuur",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
},
"openai_azure": {
"title": "Configureer OpenAI Azure",
"data": {
"openai_azure_api_key": "Azure OpenAI API-sleutel",
"openai_azure_deployment_id": "Azure Implementatie-ID",
"openai_azure_endpoint": "Azure Endpoint",
"openai_azure_api_version": "Azure API-versie",
"openai_azure_temperature": "Temperatuur",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
},
"generic_openai": {
"title": "Configureer Generieke OpenAI",
"data": {
"generic_openai_api_endpoint": "Volledige API-URL voor generieke OpenAI (moet eindigen op 'completions')",
"generic_openai_api_key": "Generieke OpenAI API-sleutel (Optioneel)",
"generic_openai_model": "Generiek OpenAI Model",
"generic_openai_temperature": "Temperatuur",
"generic_openai_validation_endpoint": "Validatie-URL (moet eindigen op 'models')",
"generic_openai_enable_validation": "Validatie Inschakelen",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
}
}
},
"error": {
"api_error": "API-validatie mislukt: {error_message}",
"cannot_connect": "Verbinding mislukt. Controleer uw instellingen en netwerk.",
"unknown": "Er is een onbekende fout opgetreden. Controleer de logs voor details."
},
"abort": {
"already_configured": "Deze AI-provider is al geconfigureerd. U kunt deze bewerken via de integratiepagina."
}
},
"options": {
"step": {
"init": {
"title": "AI Automatisering Suggester Opties",
"data": {
"openai_api_key": "OpenAI API-sleutel",
"openai_model": "OpenAI Model",
"openai_temperature": "Temperatuur (OpenAI)",
"anthropic_api_key": "Anthropic API-sleutel",
"anthropic_model": "Anthropic Model",
"anthropic_temperature": "Temperatuur (Anthropic)",
"google_api_key": "Google API-sleutel",
"google_model": "Google Model",
"google_temperature": "Temperatuur (Google)",
"groq_api_key": "Groq API-sleutel",
"groq_model": "Groq Model",
"groq_temperature": "Temperatuur (Groq)",
"localai_ip": "LocalAI IP-adres",
"localai_port": "LocalAI Poort",
"localai_https": "Gebruik HTTPS voor LocalAI",
"localai_model": "LocalAI Model",
"localai_temperature": "Temperatuur (LocalAI)",
"ollama_ip": "Ollama IP-adres",
"ollama_port": "Ollama Poort",
"ollama_https": "Gebruik HTTPS voor Ollama",
"ollama_model": "Ollama Model",
"ollama_temperature": "Temperatuur (Ollama)",
"ollama_disable_think": "Denkmodus uitschakelen (Ollama)",
"custom_openai_endpoint": "Aangepast OpenAI Eindpunt",
"custom_openai_api_key": "Aangepaste OpenAI API-sleutel",
"custom_openai_model": "Aangepast OpenAI Model",
"custom_openai_temperature": "Temperatuur (Aangepaste OpenAI)",
"mistral_api_key": "Mistral API-sleutel",
"mistral_model": "Mistral Model",
"mistral_temperature": "Temperatuur (Mistral AI)",
"perplexity_api_key": "Perplexity API-sleutel",
"perplexity_model": "Perplexity Model",
"perplexity_temperature": "Temperatuur (Perplexity AI)",
"openrouter_api_key": "OpenRouter API-sleutel",
"openrouter_model": "OpenRouter Model",
"openrouter_reasoning_max_tokens": "OpenRouter Redenering Max Tokens",
"openrouter_temperature": "Temperatuur (OpenRouter)",
"openai_azure_api_key": "Azure OpenAI API-sleutel",
"openai_azure_deployment_id": "Azure Implementatie-ID",
"openai_azure_endpoint": "Azure Eindpunt",
"openai_azure_api_version": "Azure API Versie",
"openai_azure_temperature": "Temperatuur (OpenAI Azure)",
"generic_openai_api_endpoint": "Volledige API-URL voor generieke OpenAI (moet eindigen op 'completions')",
"generic_openai_api_key": "Generieke OpenAI API-sleutel",
"generic_openai_model": "Generiek OpenAI-model",
"generic_openai_temperature": "Temperatuur (Generieke OpenAI)",
"generic_openai_validation_endpoint": "Validatie-URL (Generieke OpenAI, moet eindigen op 'models')",
"generic_openai_enable_validation": "Validatie Inschakelen (Generieke OpenAI)",
"max_input_tokens": "Maximaal aantal invoertokens",
"max_output_tokens": "Maximaal aantal uitvoertokens"
},
"description": "Pas de instellingen voor uw AI-providers aan. Alleen de velden die relevant zijn voor uw geconfigureerde provider worden gebruikt. Algemene instellingen zoals tokenlimieten worden per provider geconfigureerd."
}
},
"error": {
"invalid_input": "Een of meer waarden zijn ongeldig."
}
},
"services": {
"generate_suggestions": {
"name": "Suggesties Genereren",
"description": "Handmatig AI-automatiseringssuggesties activeren.",
"fields": {
"provider_config": {
"name": "Aanbiederconfiguratie",
"description": "Welke aanbiederconfiguratie moet worden gebruikt (indien er meerdere zijn)?"
},
"custom_prompt": {
"name": "Aangepaste Prompt",
"description": "Optionele aangepaste prompt om de standaardsysteemprompt te overschrijven of de suggesties naar specifieke thema's te leiden."
},
"all_entities": {
"name": "Alle Entiteiten Beschouwen",
"description": "Indien waar, worden alle entiteiten in overweging genomen in plaats van alleen nieuwe entiteiten."
},
"domains": {
"name": "Domeinen",
"description": "Lijst met te beschouwen domeinen. Als deze leeg is, worden alle domeinen beschouwd."
},
"entity_limit": {
"name": "Entiteitenlimiet",
"description": "Maximaal aantal te beschouwen entiteiten (willekeurig geselecteerd)."
},
"automation_read_yaml": {
"name": "Lees 'automations.yaml' bestand",
"description": "Leest en voegt de YAML-code van de automatiseringen uit het 'automations.yaml'-bestand toe. Deze actie verbruikt veel invoertokens, gebruik deze voorzichtig en met modellen met een groot contextvenster (bijv. Gemini)."
},
"automation_limit": {
"name": "Automatiseringslimiet",
"description": "Maximum aantal te analyseren automatiseringen (standaard: 100)."
}
}
}
}
}
@@ -0,0 +1,252 @@
{
"title": "Sugestor de Automações por IA",
"config": {
"step": {
"user": {
"title": "Selecionar Fornecedor de IA",
"data": {
"provider": "Fornecedor de IA"
}
},
"openai": {
"title": "Configurar OpenAI",
"data": {
"openai_api_key": "Chave API da OpenAI",
"openai_model": "Modelo da OpenAI",
"openai_temperature": "Temperatura",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
},
"anthropic": {
"title": "Configurar Anthropic",
"data": {
"anthropic_api_key": "Chave API da Anthropic",
"anthropic_model": "Modelo da Anthropic",
"anthropic_temperature": "Temperatura",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
},
"google": {
"title": "Configurar Google",
"data": {
"google_api_key": "Chave API da Google",
"google_model": "Modelo da Google",
"google_temperature": "Temperatura",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
},
"groq": {
"title": "Configurar Groq",
"data": {
"groq_api_key": "Chave API da Groq",
"groq_model": "Modelo da Groq",
"groq_temperature": "Temperatura",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
},
"localai": {
"title": "Configurar LocalAI",
"data": {
"localai_ip": "Endereço IP do LocalAI",
"localai_port": "Porta do LocalAI",
"localai_https": "Usar HTTPS para LocalAI",
"localai_model": "Modelo do LocalAI",
"localai_temperature": "Temperatura",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
},
"ollama": {
"title": "Configurar Ollama",
"data": {
"ollama_ip": "Endereço IP do Ollama",
"ollama_port": "Porta do Ollama",
"ollama_https": "Usar HTTPS para Ollama",
"ollama_model": "Modelo do Ollama",
"ollama_temperature": "Temperatura",
"ollama_disable_think": "Desativar Modo 'Pensar' (Ollama)",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
},
"custom_openai": {
"title": "Configurar OpenAI Personalizado",
"data": {
"custom_openai_endpoint": "Endpoint da OpenAI Personalizado",
"custom_openai_api_key": "Chave API da OpenAI Personalizado (Opcional)",
"custom_openai_model": "Modelo da OpenAI Personalizado",
"custom_openai_temperature": "Temperatura",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
},
"mistral": {
"title": "Configurar Mistral AI",
"data": {
"mistral_api_key": "Chave API da Mistral",
"mistral_model": "Modelo da Mistral",
"mistral_temperature": "Temperatura",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
},
"perplexity": {
"title": "Configurar Perplexity AI",
"data": {
"perplexity_api_key": "Chave API da Perplexity",
"perplexity_model": "Modelo da Perplexity",
"perplexity_temperature": "Temperatura",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
},
"openrouter": {
"title": "Configurar OpenRouter",
"data": {
"openrouter_api_key": "Chave API do OpenRouter",
"openrouter_model": "Modelo do OpenRouter",
"openrouter_reasoning_max_tokens": "Máximo de Tokens de Raciocínio do OpenRouter",
"openrouter_temperature": "Temperatura",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
},
"openai_azure": {
"title": "Configurar OpenAI Azure",
"data": {
"openai_azure_api_key": "Chave API da OpenAI Azure",
"openai_azure_deployment_id": "ID de Implementação (Deployment ID) Azure",
"openai_azure_endpoint": "Endpoint Azure",
"openai_azure_api_version": "Versão da API Azure",
"openai_azure_temperature": "Temperatura",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
},
"generic_openai": {
"title": "Configurar OpenAI Genérico",
"data": {
"generic_openai_api_endpoint": "URL Completo da API OpenAI Genérica (deve terminar em 'completions')",
"generic_openai_api_key": "Chave API da OpenAI Genérica (Opcional)",
"generic_openai_model": "Modelo da OpenAI Genérica",
"generic_openai_temperature": "Temperatura",
"generic_openai_validation_endpoint": "URL de Validação (deve terminar em 'models')",
"generic_openai_enable_validation": "Ativar Validação",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
}
}
},
"error": {
"api_error": "Falha na validação da API: {error_message}",
"cannot_connect": "Falha na ligação. Por favor, verifique as suas configurações e rede.",
"unknown": "Ocorreu um erro desconhecido. Por favor, verifique os registos para mais detalhes (logs)."
},
"abort": {
"already_configured": "Este fornecedor de IA já está configurado. Pode editá-lo na página de integrações."
}
},
"options": {
"step": {
"init": {
"title": "Opções do Sugestor de Automações por IA",
"data": {
"openai_api_key": "Chave API da OpenAI",
"openai_model": "Modelo da OpenAI",
"openai_temperature": "Temperatura (OpenAI)",
"anthropic_api_key": "Chave API da Anthropic",
"anthropic_model": "Modelo da Anthropic",
"anthropic_temperature": "Temperatura (Anthropic)",
"google_api_key": "Chave API da Google",
"google_model": "Modelo da Google",
"google_temperature": "Temperatura (Google)",
"groq_api_key": "Chave API da Groq",
"groq_model": "Modelo da Groq",
"groq_temperature": "Temperatura (Groq)",
"localai_ip": "Endereço IP do LocalAI",
"localai_port": "Porta do LocalAI",
"localai_https": "Usar HTTPS para LocalAI",
"localai_model": "Modelo do LocalAI",
"localai_temperature": "Temperatura (LocalAI)",
"ollama_ip": "Endereço IP do Ollama",
"ollama_port": "Porta do Ollama",
"ollama_https": "Usar HTTPS para Ollama",
"ollama_model": "Modelo do Ollama",
"ollama_temperature": "Temperatura (Ollama)",
"ollama_disable_think": "Desativar Modo 'Pensar' (Ollama)",
"custom_openai_endpoint": "Endpoint da OpenAI Personalizado",
"custom_openai_api_key": "Chave API da OpenAI Personalizado",
"custom_openai_model": "Modelo da OpenAI Personalizado",
"custom_openai_temperature": "Temperatura (OpenAI Personalizado)",
"mistral_api_key": "Chave API da Mistral",
"mistral_model": "Modelo da Mistral",
"mistral_temperature": "Temperatura (Mistral AI)",
"perplexity_api_key": "Chave API da Perplexity",
"perplexity_model": "Modelo da Perplexity",
"perplexity_temperature": "Temperatura (Perplexity AI)",
"openrouter_api_key": "Chave API do OpenRouter",
"openrouter_model": "Modelo do OpenRouter",
"openrouter_reasoning_max_tokens": "Máximo de Tokens de Raciocínio do OpenRouter",
"openrouter_temperature": "Temperatura (OpenRouter)",
"openai_azure_api_key": "Chave API da OpenAI Azure",
"openai_azure_deployment_id": "ID de Implementação (Deployment ID) Azure",
"openai_azure_endpoint": "Endpoint Azure",
"openai_azure_api_version": "Versão da API Azure",
"openai_azure_temperature": "Temperatura (OpenAI Azure)",
"generic_openai_api_endpoint": "URL Completo da API OpenAI Genérica (deve terminar em 'completions')",
"generic_openai_api_key": "Chave API da OpenAI Genérica",
"generic_openai_model": "Modelo da OpenAI Genérica",
"generic_openai_temperature": "Temperatura (OpenAI Genérica)",
"generic_openai_validation_endpoint": "URL de Validação (OpenAI Genérica, deve terminar em 'models')",
"generic_openai_enable_validation": "Ativar Validação (OpenAI Genérica)",
"max_input_tokens": "Máximo de Tokens de Entrada",
"max_output_tokens": "Máximo de Tokens de Saída"
},
"description": "Ajuste as configurações para os seus fornecedores de IA. Os campos relevantes para o seu fornecedor configurado serão utilizados. As configurações comuns, como limites de tokens, são definidas por fornecedor."
}
},
"error": {
"invalid_input": "Um ou mais valores são inválidos."
}
},
"services": {
"generate_suggestions": {
"name": "Gerar Sugestões",
"description": "Acionar manualmente as sugestões de automação por IA.",
"fields": {
"provider_config": {
"name": "Configuração do Fornecedor",
"description": "Qual configuração de fornecedor utilizar (caso tenha várias)"
},
"custom_prompt": {
"name": "Prompt Personalizado",
"description": "Prompt personalizado opcional para anular o prompt do sistema predefinido ou guiar as sugestões para temas específicos"
},
"all_entities": {
"name": "Considerar Todas as Entidades",
"description": "Se verdadeiro, considera todas as entidades em vez de apenas novas entidades."
},
"domains": {
"name": "Domínios",
"description": "Lista de domínios a considerar. Se vazio, considera todos os domínios."
},
"entity_limit": {
"name": "Limite de Entidades",
"description": "Número máximo de entidades a considerar (selecionadas aleatoriamente)."
},
"automation_read_yaml": {
"name": "Ler ficheiro 'automations.yaml'",
"description": "Lê e anexa o código YAML das automações encontradas no ficheiro 'automations.yaml'. Esta ação irá usar muitos tokens de entrada; use-a com cautela e com modelos com uma janela de contexto grande (ex: Gemini)."
},
"automation_limit": {
"name": "Limite de Automações",
"description": "Número máximo de automações a analisar (predefinição: 100)."
}
}
}
}
}
@@ -0,0 +1,252 @@
{
"title": "AI Automation Suggester",
"config": {
"step": {
"user": {
"title": "Выберите Провайдера ИИ",
"data": {
"provider": "Провайдер ИИ"
}
},
"openai": {
"title": "Настроить OpenAI",
"data": {
"openai_api_key": "Ключ API OpenAI",
"openai_model": "Модель OpenAI",
"openai_temperature": "Температура",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
},
"anthropic": {
"title": "Настроить Anthropic",
"data": {
"anthropic_api_key": "Ключ API Anthropic",
"anthropic_model": "Модель Anthropic",
"anthropic_temperature": "Температура",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
},
"google": {
"title": "Настроить Google",
"data": {
"google_api_key": "Ключ API Google",
"google_model": "Модель Google",
"google_temperature": "Температура",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
},
"groq": {
"title": "Настроить Groq",
"data": {
"groq_api_key": "Ключ API Groq",
"groq_model": "Модель Groq",
"groq_temperature": "Температура",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
},
"localai": {
"title": "Настроить LocalAI",
"data": {
"localai_ip": "IP-адрес LocalAI",
"localai_port": "Порт LocalAI",
"localai_https": "Использовать HTTPS для LocalAI",
"localai_model": "Модель LocalAI",
"localai_temperature": "Температура",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
},
"ollama": {
"title": "Настроить Ollama",
"data": {
"ollama_ip": "IP-адрес Ollama",
"ollama_port": "Порт Ollama",
"ollama_https": "Использовать HTTPS для Ollama",
"ollama_model": "Модель Ollama",
"ollama_temperature": "Температура",
"ollama_disable_think": "Отключить Режим Размышления (Ollama)",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
},
"custom_openai": {
"title": "Настроить Пользовательский OpenAI",
"data": {
"custom_openai_endpoint": "Пользовательский Endpoint OpenAI",
"custom_openai_api_key": "Пользовательский Ключ API OpenAI (Необязательно)",
"custom_openai_model": "Пользовательская Модель OpenAI",
"custom_openai_temperature": "Температура",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
},
"mistral": {
"title": "Настроить Mistral AI",
"data": {
"mistral_api_key": "Ключ API Mistral",
"mistral_model": "Модель Mistral",
"mistral_temperature": "Температура",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
},
"perplexity": {
"title": "Настроить Perplexity AI",
"data": {
"perplexity_api_key": "Ключ API Perplexity",
"perplexity_model": "Модель Perplexity",
"perplexity_temperature": "Температура",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
},
"openrouter": {
"title": "Настроить OpenRouter",
"data": {
"openrouter_api_key": "Ключ API OpenRouter",
"openrouter_model": "Модель OpenRouter",
"openrouter_reasoning_max_tokens": "Макс. Токенов для Размышлений OpenRouter",
"openrouter_temperature": "Температура",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
},
"openai_azure": {
"title": "Настройка OpenAI Azure",
"data": {
"openai_azure_api_key": "Ключ API Azure OpenAI",
"openai_azure_deployment_id": "ID развертывания Azure",
"openai_azure_endpoint": "Конечная точка Azure",
"openai_azure_api_version": "Версия API Azure",
"openai_azure_temperature": "Температура",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
},
"generic_openai": {
"title": "Настройка универсального OpenAI",
"data": {
"generic_openai_api_endpoint": "Полный URL-адрес API универсального OpenAI (должен заканчиваться на 'completions')",
"generic_openai_api_key": "Ключ API универсального OpenAI (опционально)",
"generic_openai_model": "Модель универсального OpenAI",
"generic_openai_temperature": "Температура",
"generic_openai_validation_endpoint": "URL-адрес для проверки (должен заканчиваться на 'models')",
"generic_openai_enable_validation": "Включить валидацию",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
}
}
},
"error": {
"api_error": "Ошибка проверки API: {error_message}",
"cannot_connect": "Не удалось подключиться. Проверьте настройки и сеть.",
"unknown": "Произошла неизвестная ошибка. Проверьте логи для получения подробной информации."
},
"abort": {
"already_configured": "Этот провайдер ИИ уже настроен. Вы можете отредактировать его на странице интеграций."
}
},
"options": {
"step": {
"init": {
"title": "Настройки Советчика Автоматизаций с ИИ",
"data": {
"openai_api_key": "Ключ API OpenAI",
"openai_model": "Модель OpenAI",
"openai_temperature": "Температура (OpenAI)",
"anthropic_api_key": "Ключ API Anthropic",
"anthropic_model": "Модель Anthropic",
"anthropic_temperature": "Температура (Anthropic)",
"google_api_key": "Ключ API Google",
"google_model": "Модель Google",
"google_temperature": "Температура (Google)",
"groq_api_key": "Ключ API Groq",
"groq_model": "Модель Groq",
"groq_temperature": "Температура (Groq)",
"localai_ip": "IP-адрес LocalAI",
"localai_port": "Порт LocalAI",
"localai_https": "Использовать HTTPS для LocalAI",
"localai_model": "Модель LocalAI",
"localai_temperature": "Температура (LocalAI)",
"ollama_ip": "IP-адрес Ollama",
"ollama_port": "Порт Ollama",
"ollama_https": "Использовать HTTPS для Ollama",
"ollama_model": "Модель Ollama",
"ollama_temperature": "Температура (Ollama)",
"ollama_disable_think": "Отключить Режим Размышления (Ollama)",
"custom_openai_endpoint": "Пользовательский Endpoint OpenAI",
"custom_openai_api_key": "Пользовательский Ключ API OpenAI",
"custom_openai_model": "Пользовательская Модель OpenAI",
"custom_openai_temperature": "Температура (Пользовательский OpenAI)",
"mistral_api_key": "Ключ API Mistral",
"mistral_model": "Модель Mistral",
"mistral_temperature": "Температура (Mistral AI)",
"perplexity_api_key": "Ключ API Perplexity",
"perplexity_model": "Модель Perplexity",
"perplexity_temperature": "Температура (Perplexity AI)",
"openrouter_api_key": "Ключ API OpenRouter",
"openrouter_model": "Модель OpenRouter",
"openrouter_reasoning_max_tokens": "Макс. Токенов для Размышлений OpenRouter",
"openrouter_temperature": "Температура (OpenRouter)",
"openai_azure_api_key": "Ключ API Azure OpenAI",
"openai_azure_deployment_id": "ID Развертывания Azure",
"openai_azure_endpoint": "Endpoint Azure",
"openai_azure_api_version": "Версия API Azure",
"openai_azure_temperature": "Температура (OpenAI Azure)",
"generic_openai_api_endpoint": "Полный URL-адрес API для Общего OpenAI (должен заканчиваться на 'completions')",
"generic_openai_api_key": "Ключ API Общего OpenAI",
"generic_openai_model": "Модель Общего OpenAI",
"generic_openai_temperature": "Температура (Общий OpenAI)",
"generic_openai_validation_endpoint": "URL-адрес для проверки (Общий OpenAI, должен заканчиваться на 'models')",
"generic_openai_enable_validation": "Включить валидацию (Общий OpenAI)",
"max_input_tokens": "Максимальное количество входных токенов",
"max_output_tokens": "Максимальное количество выходных токенов"
},
"description": "Настройте параметры для ваших провайдеров ИИ. Будут использоваться только те поля, которые относятся к настроенному вами провайдеру. Общие параметры, такие как лимиты токенов, настраиваются для каждого провайдера."
}
},
"error": {
"invalid_input": "Одно или несколько значений недействительны."
}
},
"services": {
"generate_suggestions": {
"name": "Сгенерировать Предложения",
"description": "Вручную запустить генерацию предложений по автоматизации с ИИ.",
"fields": {
"provider_config": {
"name": "Конфигурация провайдера",
"description": "Какую конфигурацию провайдера использовать (если у вас несколько)"
},
"custom_prompt": {
"name": "Пользовательский запрос",
"description": "Необязательный пользовательский запрос для переопределения системного запроса по умолчанию или направления предложений к определённым темам."
},
"all_entities": {
"name": "Учитывать все сущности",
"description": "Если включено, учитываются все сущности, а не только новые."
},
"domains": {
"name": "Домены",
"description": "Список доменов для рассмотрения. Если пусто, учитываются все домены."
},
"entity_limit": {
"name": "Лимит сущностей",
"description": "Максимальное количество сущностей для рассмотрения (выбирается случайным образом)."
},
"automation_read_yaml": {
"name": "Прочитать файл 'automations.yaml'",
"description": "Читает и добавляет YAML-код автоматизаций, найденных в файле 'automations.yaml'. Это действие использует много входных токенов, используйте его с осторожностью и с моделями с большим окном контекста (например, Gemini)."
},
"automation_limit": {
"name": "Лимит автоматизаций",
"description": "Максимальное количество автоматизаций для анализа (по умолчанию: 100)."
}
}
}
}
}
@@ -0,0 +1,252 @@
{
"title": "AI Otomasyon Önericisi",
"config": {
"step": {
"user": {
"title": "AI Sağlayıcı Seçin",
"data": {
"provider": "AI Sağlayıcı"
}
},
"openai": {
"title": "OpenAI Yapılandırması",
"data": {
"openai_api_key": "OpenAI API Anahtarı",
"openai_model": "OpenAI Modeli",
"openai_temperature": "Sıcaklık",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
},
"anthropic": {
"title": "Anthropic Yapılandırması",
"data": {
"anthropic_api_key": "Anthropic API Anahtarı",
"anthropic_model": "Anthropic Modeli",
"anthropic_temperature": "Sıcaklık",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
},
"google": {
"title": "Google Yapılandırması",
"data": {
"google_api_key": "Google API Anahtarı",
"google_model": "Google Modeli",
"google_temperature": "Sıcaklık",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
},
"groq": {
"title": "Groq Yapılandırması",
"data": {
"groq_api_key": "Groq API Anahtarı",
"groq_model": "Groq Modeli",
"groq_temperature": "Sıcaklık",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
},
"localai": {
"title": "LocalAI Yapılandırması",
"data": {
"localai_ip": "LocalAI IP Adresi",
"localai_port": "LocalAI Port",
"localai_https": "LocalAI için HTTPS Kullan",
"localai_model": "LocalAI Modeli",
"localai_temperature": "Sıcaklık",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
},
"ollama": {
"title": "Ollama Yapılandırması",
"data": {
"ollama_ip": "Ollama IP Adresi",
"ollama_port": "Ollama Port",
"ollama_https": "Ollama için HTTPS Kullan",
"ollama_model": "Ollama Modeli",
"ollama_temperature": "Sıcaklık",
"ollama_disable_think": "Düşünme Modunu Devre Dışı Bırak (Ollama)",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
},
"custom_openai": {
"title": "Özel OpenAI Yapılandırması",
"data": {
"custom_openai_endpoint": "Özel OpenAI Uç Noktası",
"custom_openai_api_key": "Özel OpenAI API Anahtarı (İsteğe Bağlı)",
"custom_openai_model": "Özel OpenAI Modeli",
"custom_openai_temperature": "Sıcaklık",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
},
"mistral": {
"title": "Mistral AI Yapılandırması",
"data": {
"mistral_api_key": "Mistral API Anahtarı",
"mistral_model": "Mistral Modeli",
"mistral_temperature": "Sıcaklık",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
},
"perplexity": {
"title": "Perplexity AI Yapılandırması",
"data": {
"perplexity_api_key": "Perplexity API Anahtarı",
"perplexity_model": "Perplexity Modeli",
"perplexity_temperature": "Sıcaklık",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
},
"openrouter": {
"title": "OpenRouter Yapılandırması",
"data": {
"openrouter_api_key": "OpenRouter API Anahtarı",
"openrouter_model": "OpenRouter Modeli",
"openrouter_reasoning_max_tokens": "OpenRouter Akıl Yürütme Maks Token",
"openrouter_temperature": "Sıcaklık",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
},
"openai_azure": {
"title": "OpenAI Azure Yapılandırması",
"data": {
"openai_azure_api_key": "Azure OpenAI API Anahtarı",
"openai_azure_deployment_id": "Azure Dağıtım Kimliği",
"openai_azure_endpoint": "Azure Uç Noktası",
"openai_azure_api_version": "Azure API Sürümü",
"openai_azure_temperature": "Sıcaklık",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
},
"generic_openai": {
"title": "Genel OpenAI Yapılandırması",
"data": {
"generic_openai_api_endpoint": "Genel OpenAI API Tam URL (sonunda 'completions' olmalı)",
"generic_openai_api_key": "Genel OpenAI API Anahtarı (İsteğe Bağlı)",
"generic_openai_model": "Genel OpenAI Modeli",
"generic_openai_temperature": "Sıcaklık",
"generic_openai_validation_endpoint": "Doğrulama URL'si (sonunda 'models' olmalı)",
"generic_openai_enable_validation": "Doğrulamayı Etkinleştir",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
}
}
},
"error": {
"api_error": "API doğrulaması başarısız oldu: {error_message}",
"cannot_connect": "Bağlantı kurulamadı. Lütfen ayarlarınızı ve ağ bağlantınızı kontrol edin.",
"unknown": "Bilinmeyen bir hata oluştu. Lütfen ayrıntılar için günlükleri kontrol edin."
},
"abort": {
"already_configured": "Bu AI sağlayıcı zaten yapılandırılmış. Entegrasyonlar sayfasından düzenleyebilirsiniz."
}
},
"options": {
"step": {
"init": {
"title": "AI Otomasyon Önericisi Seçenekleri",
"data": {
"openai_api_key": "OpenAI API Anahtarı",
"openai_model": "OpenAI Modeli",
"openai_temperature": "Sıcaklık (OpenAI)",
"anthropic_api_key": "Anthropic API Anahtarı",
"anthropic_model": "Anthropic Modeli",
"anthropic_temperature": "Sıcaklık (Anthropic)",
"google_api_key": "Google API Anahtarı",
"google_model": "Google Modeli",
"google_temperature": "Sıcaklık (Google)",
"groq_api_key": "Groq API Anahtarı",
"groq_model": "Groq Modeli",
"groq_temperature": "Sıcaklık (Groq)",
"localai_ip": "LocalAI IP Adresi",
"localai_port": "LocalAI Port",
"localai_https": "LocalAI için HTTPS Kullan",
"localai_model": "LocalAI Modeli",
"localai_temperature": "Sıcaklık (LocalAI)",
"ollama_ip": "Ollama IP Adresi",
"ollama_port": "Ollama Port",
"ollama_https": "Ollama için HTTPS Kullan",
"ollama_model": "Ollama Modeli",
"ollama_temperature": "Sıcaklık (Ollama)",
"ollama_disable_think": "Düşünme Modunu Devre Dışı Bırak (Ollama)",
"custom_openai_endpoint": "Özel OpenAI Uç Noktası",
"custom_openai_api_key": "Özel OpenAI API Anahtarı",
"custom_openai_model": "Özel OpenAI Modeli",
"custom_openai_temperature": "Sıcaklık (Özel OpenAI)",
"mistral_api_key": "Mistral API Anahtarı",
"mistral_model": "Mistral Modeli",
"mistral_temperature": "Sıcaklık (Mistral AI)",
"perplexity_api_key": "Perplexity API Anahtarı",
"perplexity_model": "Perplexity Modeli",
"perplexity_temperature": "Sıcaklık (Perplexity AI)",
"openrouter_api_key": "OpenRouter API Anahtarı",
"openrouter_model": "OpenRouter Modeli",
"openrouter_reasoning_max_tokens": "OpenRouter Akıl Yürütme Maks Token",
"openrouter_temperature": "Sıcaklık (OpenRouter)",
"openai_azure_api_key": "Azure OpenAI API Anahtarı",
"openai_azure_deployment_id": "Azure Dağıtım Kimliği",
"openai_azure_endpoint": "Azure Uç Noktası",
"openai_azure_api_version": "Azure API Sürümü",
"openai_azure_temperature": "Sıcaklık (OpenAI Azure)",
"generic_openai_api_endpoint": "Genel OpenAI API Tam URL (sonunda 'completions' olmalı)",
"generic_openai_api_key": "Genel OpenAI API Anahtarı",
"generic_openai_model": "Genel OpenAI Modeli",
"generic_openai_temperature": "Sıcaklık (Genel OpenAI)",
"generic_openai_validation_endpoint": "Doğrulama URL'si (Genel OpenAI, sonunda 'models' olmalı)",
"generic_openai_enable_validation": "Doğrulamayı Etkinleştir (Genel OpenAI)",
"max_input_tokens": "Maksimum Girdi Token",
"max_output_tokens": "Maksimum Çıktı Token"
},
"description": "AI sağlayıcılarınız için ayarları düzenleyin. Yapılandırılmış sağlayıcınızla ilgili alanlar kullanılacaktır. Token limitleri gibi ortak ayarlar her sağlayıcı için ayrı ayrı yapılandırılır."
}
},
"error": {
"invalid_input": "Bir veya daha fazla değer geçersiz."
}
},
"services": {
"generate_suggestions": {
"name": "Öneri Oluştur",
"description": "AI otomasyon önerilerini manuel olarak tetikleyin.",
"fields": {
"provider_config": {
"name": "Sağlayıcı Yapılandırması",
"description": "Hangi sağlayıcı yapılandırmasının kullanılacağı (birden fazla varsa)"
},
"custom_prompt": {
"name": "Özel İstem",
"description": "Varsayılan sistem istemini geçersiz kılmak veya önerileri belirli temalara yönlendirmek için isteğe bağlı özel istem"
},
"all_entities": {
"name": "Tüm Varlıkları Değerlendir",
"description": "Doğruysa, sadece yeni varlıklar yerine tüm varlıkları değerlendirir."
},
"domains": {
"name": "Alan Adları",
"description": "Değerlendirilecek alan adlarının listesi. Boşsa, tüm alan adları değerlendirilir."
},
"entity_limit": {
"name": "Varlık Limiti",
"description": "Değerlendirilecek maksimum varlık sayısı (rastgele seçilir)."
},
"automation_read_yaml": {
"name": "'automations.yaml' dosyasını oku",
"description": "'automations.yaml' dosyasında bulunan otomasyonların YAML kodunu okur ve ekler. Bu işlem çok fazla girdi token'ı kullanır, büyük bağlam penceresine sahip modellerle (örn. Gemini) dikkatli kullanın."
},
"automation_limit": {
"name": "Otomasyon Limiti",
"description": "Analiz edilecek maksimum otomasyon sayısı (varsayılan: 100)."
}
}
}
}
}
@@ -0,0 +1,252 @@
{
"title": "AI Automation Suggester",
"config": {
"step": {
"user": {
"title": "选择 AI 提供商",
"data": {
"provider": "AI 提供商"
}
},
"openai": {
"title": "配置 OpenAI",
"data": {
"openai_api_key": "OpenAI API 密钥",
"openai_model": "OpenAI 模型",
"openai_temperature": "温度",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
},
"anthropic": {
"title": "配置 Anthropic",
"data": {
"anthropic_api_key": "Anthropic API 密钥",
"anthropic_model": "Anthropic 模型",
"anthropic_temperature": "温度",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
},
"google": {
"title": "配置 Google",
"data": {
"google_api_key": "Google API 密钥",
"google_model": "Google 模型",
"google_temperature": "温度",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
},
"groq": {
"title": "配置 Groq",
"data": {
"groq_api_key": "Groq API 密钥",
"groq_model": "Groq 模型",
"groq_temperature": "温度",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
},
"localai": {
"title": "配置 LocalAI",
"data": {
"localai_ip": "LocalAI IP 地址",
"localai_port": "LocalAI 端口",
"localai_https": "为 LocalAI 使用 HTTPS",
"localai_model": "LocalAI 模型",
"localai_temperature": "温度",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
},
"ollama": {
"title": "配置 Ollama",
"data": {
"ollama_ip": "Ollama IP 地址",
"ollama_port": "Ollama 端口",
"ollama_https": "为 Ollama 使用 HTTPS",
"ollama_model": "Ollama 模型",
"ollama_temperature": "温度",
"ollama_disable_think": "禁用思考模式 (Ollama)",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
},
"custom_openai": {
"title": "配置自定义 OpenAI",
"data": {
"custom_openai_endpoint": "自定义 OpenAI 端点",
"custom_openai_api_key": "自定义 OpenAI API 密钥 (可选)",
"custom_openai_model": "自定义 OpenAI 模型",
"custom_openai_temperature": "温度",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
},
"mistral": {
"title": "配置 Mistral AI",
"data": {
"mistral_api_key": "Mistral API 密钥",
"mistral_model": "Mistral 模型",
"mistral_temperature": "温度",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
},
"perplexity": {
"title": "配置 Perplexity AI",
"data": {
"perplexity_api_key": "Perplexity API 密钥",
"perplexity_model": "Perplexity 模型",
"perplexity_temperature": "温度",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
},
"openrouter": {
"title": "配置 OpenRouter",
"data": {
"openrouter_api_key": "OpenRouter API 密钥",
"openrouter_model": "OpenRouter 模型",
"openrouter_reasoning_max_tokens": "OpenRouter 推理最大令牌数",
"openrouter_temperature": "温度",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
},
"openai_azure": {
"title": "配置 OpenAI Azure",
"data": {
"openai_azure_api_key": "Azure OpenAI API 密钥",
"openai_azure_deployment_id": "Azure 部署 ID",
"openai_azure_endpoint": "Azure 端点",
"openai_azure_api_version": "Azure API 版本",
"openai_azure_temperature": "温度",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
},
"generic_openai": {
"title": "配置通用 OpenAI",
"data": {
"generic_openai_api_endpoint": "通用 OpenAI 完整 API URL (应以 'completions' 结尾)",
"generic_openai_api_key": "通用 OpenAI API 密钥(可选)",
"generic_openai_model": "通用 OpenAI 模型",
"generic_openai_temperature": "温度",
"generic_openai_validation_endpoint": "验证 URL (应以 'models' 结尾)",
"generic_openai_enable_validation": "启用验证",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
}
}
},
"error": {
"api_error": "API 验证失败:{error_message}",
"cannot_connect": "连接失败。请检查您的设置和网络。",
"unknown": "发生未知错误。请检查日志以获取详细信息。"
},
"abort": {
"already_configured": "此 AI 提供商已配置。您可以从集成页面编辑它。"
}
},
"options": {
"step": {
"init": {
"title": "AI 自动化建议器选项",
"data": {
"openai_api_key": "OpenAI API 密钥",
"openai_model": "OpenAI 模型",
"openai_temperature": "温度 (OpenAI)",
"anthropic_api_key": "Anthropic API 密钥",
"anthropic_model": "Anthropic 模型",
"anthropic_temperature": "温度 (Anthropic)",
"google_api_key": "Google API 密钥",
"google_model": "Google 模型",
"google_temperature": "温度 (Google)",
"groq_api_key": "Groq API 密钥",
"groq_model": "Groq 模型",
"groq_temperature": "温度 (Groq)",
"localai_ip": "LocalAI IP 地址",
"localai_port": "LocalAI 端口",
"localai_https": "为 LocalAI 使用 HTTPS",
"localai_model": "LocalAI 模型",
"localai_temperature": "温度 (LocalAI)",
"ollama_ip": "Ollama IP 地址",
"ollama_port": "Ollama 端口",
"ollama_https": "为 Ollama 使用 HTTPS",
"ollama_model": "Ollama 模型",
"ollama_temperature": "温度 (Ollama)",
"ollama_disable_think": "禁用思考模式 (Ollama)",
"custom_openai_endpoint": "自定义 OpenAI 端点",
"custom_openai_api_key": "自定义 OpenAI API 密钥",
"custom_openai_model": "自定义 OpenAI 模型",
"custom_openai_temperature": "温度 (自定义 OpenAI)",
"mistral_api_key": "Mistral API 密钥",
"mistral_model": "Mistral 模型",
"mistral_temperature": "温度 (Mistral AI)",
"perplexity_api_key": "Perplexity API 密钥",
"perplexity_model": "Perplexity 模型",
"perplexity_temperature": "温度 (Perplexity AI)",
"openrouter_api_key": "OpenRouter API 密钥",
"openrouter_model": "OpenRouter 模型",
"openrouter_reasoning_max_tokens": "OpenRouter 推理最大令牌数",
"openrouter_temperature": "温度 (OpenRouter)",
"openai_azure_api_key": "Azure OpenAI API 密钥",
"openai_azure_deployment_id": "Azure 部署 ID",
"openai_azure_endpoint": "Azure 端点",
"openai_azure_api_version": "Azure API 版本",
"openai_azure_temperature": "温度 (OpenAI Azure)",
"generic_openai_api_endpoint": "通用 OpenAI API 完整 URL (应以 'completions' 结尾)",
"generic_openai_api_key": "通用 OpenAI API 密钥",
"generic_openai_model": "通用 OpenAI 模型",
"generic_openai_temperature": "温度 (通用 OpenAI)",
"generic_openai_validation_endpoint": "验证 URL (通用 OpenAI,应以 'models' 结尾)",
"generic_openai_enable_validation": "启用验证 (通用 OpenAI)",
"max_input_tokens": "最大输入令牌",
"max_output_tokens": "最大输出令牌"
},
"description": "调整您的 AI 提供商设置。将使用与您配置的提供商相关的字段。令牌限制等通用设置是按提供商配置的。"
}
},
"error": {
"invalid_input": "一个或多个值无效。"
}
},
"services": {
"generate_suggestions": {
"name": "生成建议",
"description": "手动触发 AI 自动化建议。",
"fields": {
"provider_config": {
"name": "提供商配置",
"description": "选择要使用的提供商配置(如果有多个)"
},
"custom_prompt": {
"name": "自定义提示",
"description": "可选的自定义提示,用于覆盖默认系统提示或将建议引导至特定主题。"
},
"all_entities": {
"name": "考虑所有实体",
"description": "若为 true,则考虑所有实体而不仅仅是新实体。"
},
"domains": {
"name": "领域",
"description": "要考虑的领域列表。如果为空,则考虑所有领域。"
},
"entity_limit": {
"name": "实体限制",
"description": "要考虑的实体最大数量(随机选择)。"
},
"automation_read_yaml": {
"name": "读取 'automations.yaml' 文件",
"description": "读取并附加 'automations.yaml' 文件中找到的自动化的 YAML 代码。此操作将使用大量输入令牌,请谨慎使用,并配合具有较大上下文窗口的模型(例如 Gemini)。"
},
"automation_limit": {
"name": "自动化数量上限",
"description": "要分析的自动化最大数量(默认:100)。"
}
}
}
}
}
@@ -0,0 +1,159 @@
import { LitElement, html, css } from "lit";
class AiAutomationSuggesterCard extends LitElement {
static properties = {
hass: { attribute: false },
suggestions: { state: true },
loading: { state: true },
error: { state: true },
};
static styles = css`
:host {
display: block;
}
ha-card {
padding: 16px;
}
.toolbar {
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
li {
border-top: 1px solid var(--divider-color, #ddd);
padding: 12px 0;
}
li:first-child {
border-top: 0;
}
pre {
background: var(--code-editor-background-color, #f5f5f5);
border-radius: 6px;
overflow-x: auto;
padding: 12px;
white-space: pre-wrap;
}
.meta {
color: var(--secondary-text-color);
font-size: 0.9em;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.error {
color: var(--error-color, #b00020);
}
`;
constructor() {
super();
this.suggestions = [];
this.loading = true;
this.error = null;
}
connectedCallback() {
super.connectedCallback();
this.fetchData();
}
setConfig(config) {
this.config = config;
}
async fetchData() {
if (!this.hass) {
return;
}
this.loading = true;
this.error = null;
try {
this.suggestions = await this.hass.callApi("GET", "ai_automation_suggester/suggestions");
} catch (err) {
this.error = err.message || "Failed to fetch suggestions.";
} finally {
this.loading = false;
}
}
async copyYaml(yaml) {
await navigator.clipboard.writeText(yaml || "");
this.dispatchEvent(
new CustomEvent("hass-notification", {
detail: { type: "info", message: "YAML copied to clipboard." },
bubbles: true,
composed: true,
}),
);
}
async handleSuggestionAction(suggestionId, action) {
try {
const response = await this.hass.callApi(
"POST",
`ai_automation_suggester/${action}/${suggestionId}`,
);
if (response.success) {
await this.fetchData();
} else {
this.error = response.error || `Failed to ${action} suggestion.`;
}
} catch (err) {
this.error = err.message || `Failed to ${action} suggestion.`;
}
}
renderSuggestion(suggestion) {
const yamlCode = suggestion.yamlCode || suggestion.yaml_block || "";
return html`
<li>
<h3>${suggestion.title || "AI automation suggestion"}</h3>
<p>${suggestion.shortDescription || suggestion.description || "No description returned."}</p>
<p class="meta">
${suggestion.provider || "Unknown provider"} - ${suggestion.model || "Unknown model"} -
${suggestion.status || "new"}
</p>
${yamlCode ? html`<pre><code>${yamlCode}</code></pre>` : html`<p class="meta">No YAML was returned.</p>`}
${(suggestion.warnings || []).length
? html`<p class="meta">${suggestion.warnings.join(" ")}</p>`
: ""}
<div class="actions">
<ha-button @click=${() => this.copyYaml(yamlCode)} .disabled=${!yamlCode}>Copy YAML</ha-button>
<ha-button @click=${() => this.handleSuggestionAction(suggestion.id, "accept")}>Accept</ha-button>
<ha-button @click=${() => this.handleSuggestionAction(suggestion.id, "decline")}>Decline</ha-button>
<ha-button @click=${() => this.handleSuggestionAction(suggestion.id, "dismiss")}>Dismiss</ha-button>
</div>
</li>
`;
}
render() {
return html`
<ha-card>
<div class="toolbar">
<h2>AI Automation Suggestions</h2>
<ha-button @click=${this.fetchData}>Refresh</ha-button>
</div>
${this.loading
? html`<p>Loading suggestions...</p>`
: this.error
? html`<p class="error">${this.error}</p>`
: this.suggestions.length
? html`<ul>${this.suggestions.map((suggestion) => this.renderSuggestion(suggestion))}</ul>`
: html`<p>No stored suggestions yet.</p>`}
</ha-card>
`;
}
}
customElements.define("ai-automation-suggester-card", AiAutomationSuggesterCard);