Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Google Generative AI Conversation integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from functools import partial
7 import logging
8 from types import MappingProxyType
9 from typing import Any
10 
11 from google.ai import generativelanguage_v1beta
12 from google.api_core.client_options import ClientOptions
13 from google.api_core.exceptions import ClientError, GoogleAPIError
14 import google.generativeai as genai
15 import voluptuous as vol
16 
17 from homeassistant.config_entries import (
18  SOURCE_REAUTH,
19  ConfigEntry,
20  ConfigFlow,
21  ConfigFlowResult,
22  OptionsFlow,
23 )
24 from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API, CONF_NAME
25 from homeassistant.core import HomeAssistant
26 from homeassistant.helpers import llm
28  NumberSelector,
29  NumberSelectorConfig,
30  SelectOptionDict,
31  SelectSelector,
32  SelectSelectorConfig,
33  SelectSelectorMode,
34  TemplateSelector,
35 )
36 
37 from .const import (
38  CONF_CHAT_MODEL,
39  CONF_DANGEROUS_BLOCK_THRESHOLD,
40  CONF_HARASSMENT_BLOCK_THRESHOLD,
41  CONF_HATE_BLOCK_THRESHOLD,
42  CONF_MAX_TOKENS,
43  CONF_PROMPT,
44  CONF_RECOMMENDED,
45  CONF_SEXUAL_BLOCK_THRESHOLD,
46  CONF_TEMPERATURE,
47  CONF_TOP_K,
48  CONF_TOP_P,
49  DOMAIN,
50  RECOMMENDED_CHAT_MODEL,
51  RECOMMENDED_HARM_BLOCK_THRESHOLD,
52  RECOMMENDED_MAX_TOKENS,
53  RECOMMENDED_TEMPERATURE,
54  RECOMMENDED_TOP_K,
55  RECOMMENDED_TOP_P,
56 )
57 
58 _LOGGER = logging.getLogger(__name__)
59 
60 STEP_API_DATA_SCHEMA = vol.Schema(
61  {
62  vol.Required(CONF_API_KEY): str,
63  }
64 )
65 
66 RECOMMENDED_OPTIONS = {
67  CONF_RECOMMENDED: True,
68  CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
69  CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
70 }
71 
72 
73 async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
74  """Validate the user input allows us to connect.
75 
76  Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
77  """
78  client = generativelanguage_v1beta.ModelServiceAsyncClient(
79  client_options=ClientOptions(api_key=data[CONF_API_KEY])
80  )
81  await client.list_models(timeout=5.0)
82 
83 
85  """Handle a config flow for Google Generative AI Conversation."""
86 
87  VERSION = 1
88 
89  async def async_step_api(
90  self, user_input: dict[str, Any] | None = None
91  ) -> ConfigFlowResult:
92  """Handle the initial step."""
93  errors: dict[str, str] = {}
94  if user_input is not None:
95  try:
96  await validate_input(self.hass, user_input)
97  except GoogleAPIError as err:
98  if isinstance(err, ClientError) and err.reason == "API_KEY_INVALID":
99  errors["base"] = "invalid_auth"
100  else:
101  errors["base"] = "cannot_connect"
102  except Exception:
103  _LOGGER.exception("Unexpected exception")
104  errors["base"] = "unknown"
105  else:
106  if self.sourcesourcesourcesource == SOURCE_REAUTH:
107  return self.async_update_reload_and_abortasync_update_reload_and_abort(
108  self._get_reauth_entry_get_reauth_entry(),
109  data=user_input,
110  )
111  return self.async_create_entryasync_create_entryasync_create_entry(
112  title="Google Generative AI",
113  data=user_input,
114  options=RECOMMENDED_OPTIONS,
115  )
116  return self.async_show_formasync_show_formasync_show_form(
117  step_id="api",
118  data_schema=STEP_API_DATA_SCHEMA,
119  description_placeholders={
120  "api_key_url": "https://aistudio.google.com/app/apikey"
121  },
122  errors=errors,
123  )
124 
125  async def async_step_user(
126  self, user_input: dict[str, Any] | None = None
127  ) -> ConfigFlowResult:
128  """Handle the initial step."""
129  return await self.async_step_apiasync_step_api()
130 
131  async def async_step_reauth(
132  self, entry_data: Mapping[str, Any]
133  ) -> ConfigFlowResult:
134  """Handle configuration by re-auth."""
135  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
136 
138  self, user_input: dict[str, Any] | None = None
139  ) -> ConfigFlowResult:
140  """Dialog that informs the user that reauth is required."""
141  if user_input is not None:
142  return await self.async_step_apiasync_step_api()
143 
144  reauth_entry = self._get_reauth_entry_get_reauth_entry()
145  return self.async_show_formasync_show_formasync_show_form(
146  step_id="reauth_confirm",
147  description_placeholders={
148  CONF_NAME: reauth_entry.title,
149  CONF_API_KEY: reauth_entry.data.get(CONF_API_KEY, ""),
150  },
151  )
152 
153  @staticmethod
155  config_entry: ConfigEntry,
156  ) -> OptionsFlow:
157  """Create the options flow."""
158  return GoogleGenerativeAIOptionsFlow(config_entry)
159 
160 
162  """Google Generative AI config flow options handler."""
163 
164  def __init__(self, config_entry: ConfigEntry) -> None:
165  """Initialize options flow."""
166  self.last_rendered_recommendedlast_rendered_recommended = config_entry.options.get(
167  CONF_RECOMMENDED, False
168  )
169 
170  async def async_step_init(
171  self, user_input: dict[str, Any] | None = None
172  ) -> ConfigFlowResult:
173  """Manage the options."""
174  options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entryconfig_entryconfig_entry.options
175 
176  if user_input is not None:
177  if user_input[CONF_RECOMMENDED] == self.last_rendered_recommendedlast_rendered_recommended:
178  if user_input[CONF_LLM_HASS_API] == "none":
179  user_input.pop(CONF_LLM_HASS_API)
180  return self.async_create_entryasync_create_entry(title="", data=user_input)
181 
182  # Re-render the options again, now with the recommended options shown/hidden
183  self.last_rendered_recommendedlast_rendered_recommended = user_input[CONF_RECOMMENDED]
184 
185  options = {
186  CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
187  CONF_PROMPT: user_input[CONF_PROMPT],
188  CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
189  }
190 
191  schema = await google_generative_ai_config_option_schema(self.hass, options)
192  return self.async_show_formasync_show_form(
193  step_id="init",
194  data_schema=vol.Schema(schema),
195  )
196 
197 
199  hass: HomeAssistant,
200  options: dict[str, Any] | MappingProxyType[str, Any],
201 ) -> dict:
202  """Return a schema for Google Generative AI completion options."""
203  hass_apis: list[SelectOptionDict] = [
205  label="No control",
206  value="none",
207  )
208  ]
209  hass_apis.extend(
211  label=api.name,
212  value=api.id,
213  )
214  for api in llm.async_get_apis(hass)
215  )
216 
217  schema = {
218  vol.Optional(
219  CONF_PROMPT,
220  description={
221  "suggested_value": options.get(
222  CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT
223  )
224  },
225  ): TemplateSelector(),
226  vol.Optional(
227  CONF_LLM_HASS_API,
228  description={"suggested_value": options.get(CONF_LLM_HASS_API)},
229  default="none",
230  ): SelectSelector(SelectSelectorConfig(options=hass_apis)),
231  vol.Required(
232  CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
233  ): bool,
234  }
235 
236  if options.get(CONF_RECOMMENDED):
237  return schema
238 
239  api_models = await hass.async_add_executor_job(partial(genai.list_models))
240 
241  models = [
243  label=api_model.display_name,
244  value=api_model.name,
245  )
246  for api_model in sorted(api_models, key=lambda x: x.display_name)
247  if (
248  api_model.name != "models/gemini-1.0-pro" # duplicate of gemini-pro
249  and "vision" not in api_model.name
250  and "generateContent" in api_model.supported_generation_methods
251  )
252  ]
253 
254  harm_block_thresholds: list[SelectOptionDict] = [
256  label="Block none",
257  value="BLOCK_NONE",
258  ),
260  label="Block few",
261  value="BLOCK_ONLY_HIGH",
262  ),
264  label="Block some",
265  value="BLOCK_MEDIUM_AND_ABOVE",
266  ),
268  label="Block most",
269  value="BLOCK_LOW_AND_ABOVE",
270  ),
271  ]
272  harm_block_thresholds_selector = SelectSelector(
274  mode=SelectSelectorMode.DROPDOWN, options=harm_block_thresholds
275  )
276  )
277 
278  schema.update(
279  {
280  vol.Optional(
281  CONF_CHAT_MODEL,
282  description={"suggested_value": options.get(CONF_CHAT_MODEL)},
283  default=RECOMMENDED_CHAT_MODEL,
284  ): SelectSelector(
285  SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=models)
286  ),
287  vol.Optional(
288  CONF_TEMPERATURE,
289  description={"suggested_value": options.get(CONF_TEMPERATURE)},
290  default=RECOMMENDED_TEMPERATURE,
291  ): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
292  vol.Optional(
293  CONF_TOP_P,
294  description={"suggested_value": options.get(CONF_TOP_P)},
295  default=RECOMMENDED_TOP_P,
296  ): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
297  vol.Optional(
298  CONF_TOP_K,
299  description={"suggested_value": options.get(CONF_TOP_K)},
300  default=RECOMMENDED_TOP_K,
301  ): int,
302  vol.Optional(
303  CONF_MAX_TOKENS,
304  description={"suggested_value": options.get(CONF_MAX_TOKENS)},
305  default=RECOMMENDED_MAX_TOKENS,
306  ): int,
307  vol.Optional(
308  CONF_HARASSMENT_BLOCK_THRESHOLD,
309  description={
310  "suggested_value": options.get(CONF_HARASSMENT_BLOCK_THRESHOLD)
311  },
312  default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
313  ): harm_block_thresholds_selector,
314  vol.Optional(
315  CONF_HATE_BLOCK_THRESHOLD,
316  description={"suggested_value": options.get(CONF_HATE_BLOCK_THRESHOLD)},
317  default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
318  ): harm_block_thresholds_selector,
319  vol.Optional(
320  CONF_SEXUAL_BLOCK_THRESHOLD,
321  description={
322  "suggested_value": options.get(CONF_SEXUAL_BLOCK_THRESHOLD)
323  },
324  default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
325  ): harm_block_thresholds_selector,
326  vol.Optional(
327  CONF_DANGEROUS_BLOCK_THRESHOLD,
328  description={
329  "suggested_value": options.get(CONF_DANGEROUS_BLOCK_THRESHOLD)
330  },
331  default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
332  ): harm_block_thresholds_selector,
333  }
334  )
335  return schema
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:139
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
ConfigFlowResult async_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
None config_entry(self, ConfigEntry value)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
str|None source(self)
dict google_generative_ai_config_option_schema(HomeAssistant hass, dict[str, Any]|MappingProxyType[str, Any] options)
Definition: config_flow.py:201
None validate_input(HomeAssistant hass, dict[str, Any] data)
Definition: config_flow.py:73