Home Assistant Unofficial Reference 2024.12.1
llm.py
Go to the documentation of this file.
1 """Module to coordinate llm tools."""
2 
3 from __future__ import annotations
4 
5 from abc import ABC, abstractmethod
6 from collections.abc import Callable
7 from dataclasses import dataclass
8 from decimal import Decimal
9 from enum import Enum
10 from functools import cache, partial
11 from typing import Any
12 
13 import slugify as unicode_slug
14 import voluptuous as vol
15 from voluptuous_openapi import UNSUPPORTED, convert
16 
17 from homeassistant.components.climate import INTENT_GET_TEMPERATURE
19  ConversationTraceEventType,
20  async_conversation_trace_append,
21 )
22 from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER
23 from homeassistant.components.homeassistant import async_should_expose
24 from homeassistant.components.intent import async_device_supports_timers
25 from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN
26 from homeassistant.components.weather import INTENT_GET_WEATHER
27 from homeassistant.const import (
28  ATTR_DOMAIN,
29  ATTR_SERVICE,
30  EVENT_HOMEASSISTANT_CLOSE,
31  EVENT_SERVICE_REMOVED,
32 )
33 from homeassistant.core import Context, Event, HomeAssistant, callback, split_entity_id
34 from homeassistant.exceptions import HomeAssistantError
35 from homeassistant.util import yaml
36 from homeassistant.util.hass_dict import HassKey
37 from homeassistant.util.json import JsonObjectType
38 
39 from . import (
40  area_registry as ar,
41  config_validation as cv,
42  device_registry as dr,
43  entity_registry as er,
44  floor_registry as fr,
45  intent,
46  selector,
47  service,
48 )
49 from .singleton import singleton
50 
51 SCRIPT_PARAMETERS_CACHE: HassKey[dict[str, tuple[str | None, vol.Schema]]] = HassKey(
52  "llm_script_parameters_cache"
53 )
54 
55 
56 LLM_API_ASSIST = "assist"
57 
58 BASE_PROMPT = (
59  'Current time is {{ now().strftime("%H:%M:%S") }}. '
60  'Today\'s date is {{ now().strftime("%Y-%m-%d") }}.\n'
61 )
62 
63 DEFAULT_INSTRUCTIONS_PROMPT = """You are a voice assistant for Home Assistant.
64 Answer questions about the world truthfully.
65 Answer in plain text. Keep it simple and to the point.
66 """
67 
68 
69 @callback
70 def async_render_no_api_prompt(hass: HomeAssistant) -> str:
71  """Return the prompt to be used when no API is configured.
72 
73  No longer used since Home Assistant 2024.7.
74  """
75  return ""
76 
77 
78 @singleton("llm")
79 @callback
80 def _async_get_apis(hass: HomeAssistant) -> dict[str, API]:
81  """Get all the LLM APIs."""
82  return {
83  LLM_API_ASSIST: AssistAPI(hass=hass),
84  }
85 
86 
87 @callback
88 def async_register_api(hass: HomeAssistant, api: API) -> None:
89  """Register an API to be exposed to LLMs."""
90  apis = _async_get_apis(hass)
91 
92  if api.id in apis:
93  raise HomeAssistantError(f"API {api.id} is already registered")
94 
95  apis[api.id] = api
96 
97 
98 async def async_get_api(
99  hass: HomeAssistant, api_id: str, llm_context: LLMContext
100 ) -> APIInstance:
101  """Get an API."""
102  apis = _async_get_apis(hass)
103 
104  if api_id not in apis:
105  raise HomeAssistantError(f"API {api_id} not found")
106 
107  return await apis[api_id].async_get_api_instance(llm_context)
108 
109 
110 @callback
111 def async_get_apis(hass: HomeAssistant) -> list[API]:
112  """Get all the LLM APIs."""
113  return list(_async_get_apis(hass).values())
114 
115 
116 @dataclass(slots=True)
118  """Tool input to be processed."""
119 
120  platform: str
121  context: Context | None
122  user_prompt: str | None
123  language: str | None
124  assistant: str | None
125  device_id: str | None
126 
127 
128 @dataclass(slots=True)
129 class ToolInput:
130  """Tool input to be processed."""
131 
132  tool_name: str
133  tool_args: dict[str, Any]
134 
135 
136 class Tool:
137  """LLM Tool base class."""
138 
139  name: str
140  description: str | None = None
141  parameters: vol.Schema = vol.Schema({})
142 
143  @abstractmethod
144  async def async_call(
145  self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext
146  ) -> JsonObjectType:
147  """Call the tool."""
148  raise NotImplementedError
149 
150  def __repr__(self) -> str:
151  """Represent a string of a Tool."""
152  return f"<{self.__class__.__name__} - {self.name}>"
153 
154 
155 @dataclass
157  """Instance of an API to be used by an LLM."""
158 
159  api: API
160  api_prompt: str
161  llm_context: LLMContext
162  tools: list[Tool]
163  custom_serializer: Callable[[Any], Any] | None = None
164 
165  async def async_call_tool(self, tool_input: ToolInput) -> JsonObjectType:
166  """Call a LLM tool, validate args and return the response."""
168  ConversationTraceEventType.TOOL_CALL,
169  {"tool_name": tool_input.tool_name, "tool_args": tool_input.tool_args},
170  )
171 
172  for tool in self.tools:
173  if tool.name == tool_input.tool_name:
174  break
175  else:
176  raise HomeAssistantError(f'Tool "{tool_input.tool_name}" not found')
177 
178  return await tool.async_call(self.api.hass, tool_input, self.llm_context)
179 
180 
181 @dataclass(slots=True, kw_only=True)
182 class API(ABC):
183  """An API to expose to LLMs."""
184 
185  hass: HomeAssistant
186  id: str
187  name: str
188 
189  @abstractmethod
190  async def async_get_api_instance(self, llm_context: LLMContext) -> APIInstance:
191  """Return the instance of the API."""
192  raise NotImplementedError
193 
194 
196  """LLM Tool representing an Intent."""
197 
198  def __init__(
199  self,
200  name: str,
201  intent_handler: intent.IntentHandler,
202  ) -> None:
203  """Init the class."""
204  self.namename = name
205  self.descriptiondescription = (
206  intent_handler.description or f"Execute Home Assistant {self.name} intent"
207  )
208  self.extra_slotsextra_slots = None
209  if not (slot_schema := intent_handler.slot_schema):
210  return
211 
212  slot_schema = {**slot_schema}
213  extra_slots = set()
214 
215  for field in ("preferred_area_id", "preferred_floor_id"):
216  if field in slot_schema:
217  extra_slots.add(field)
218  del slot_schema[field]
219 
220  self.parametersparameters = vol.Schema(slot_schema)
221  if extra_slots:
222  self.extra_slotsextra_slots = extra_slots
223 
224  async def async_call(
225  self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext
226  ) -> JsonObjectType:
227  """Handle the intent."""
228  slots = {key: {"value": val} for key, val in tool_input.tool_args.items()}
229 
230  if self.extra_slotsextra_slots and llm_context.device_id:
231  device_reg = dr.async_get(hass)
232  device = device_reg.async_get(llm_context.device_id)
233 
234  area: ar.AreaEntry | None = None
235  floor: fr.FloorEntry | None = None
236  if device:
237  area_reg = ar.async_get(hass)
238  if device.area_id and (area := area_reg.async_get_area(device.area_id)):
239  if area.floor_id:
240  floor_reg = fr.async_get(hass)
241  floor = floor_reg.async_get_floor(area.floor_id)
242 
243  for slot_name, slot_value in (
244  ("preferred_area_id", area.id if area else None),
245  ("preferred_floor_id", floor.floor_id if floor else None),
246  ):
247  if slot_value and slot_name in self.extra_slotsextra_slots:
248  slots[slot_name] = {"value": slot_value}
249 
250  intent_response = await intent.async_handle(
251  hass=hass,
252  platform=llm_context.platform,
253  intent_type=self.namename,
254  slots=slots,
255  text_input=llm_context.user_prompt,
256  context=llm_context.context,
257  language=llm_context.language,
258  assistant=llm_context.assistant,
259  device_id=llm_context.device_id,
260  )
261  response = intent_response.as_dict()
262  del response["language"]
263  del response["card"]
264  return response
265 
266 
267 class AssistAPI(API):
268  """API exposing Assist API to LLMs."""
269 
270  IGNORE_INTENTS = {
271  INTENT_GET_TEMPERATURE,
272  INTENT_GET_WEATHER,
273  INTENT_OPEN_COVER, # deprecated
274  INTENT_CLOSE_COVER, # deprecated
275  intent.INTENT_GET_STATE,
276  intent.INTENT_NEVERMIND,
277  intent.INTENT_TOGGLE,
278  intent.INTENT_GET_CURRENT_DATE,
279  intent.INTENT_GET_CURRENT_TIME,
280  intent.INTENT_RESPOND,
281  }
282 
283  def __init__(self, hass: HomeAssistant) -> None:
284  """Init the class."""
285  super().__init__(
286  hass=hass,
287  id=LLM_API_ASSIST,
288  name="Assist",
289  )
290  self.cached_slugifycached_slugify = cache(
291  partial(unicode_slug.slugify, separator="_", lowercase=False)
292  )
293 
294  async def async_get_api_instance(self, llm_context: LLMContext) -> APIInstance:
295  """Return the instance of the API."""
296  if llm_context.assistant:
297  exposed_entities: dict | None = _get_exposed_entities(
298  self.hass, llm_context.assistant
299  )
300  else:
301  exposed_entities = None
302 
303  return APIInstance(
304  api=self,
305  api_prompt=self._async_get_api_prompt_async_get_api_prompt(llm_context, exposed_entities),
306  llm_context=llm_context,
307  tools=self._async_get_tools_async_get_tools(llm_context, exposed_entities),
308  custom_serializer=_selector_serializer,
309  )
310 
311  @callback
313  self, llm_context: LLMContext, exposed_entities: dict | None
314  ) -> str:
315  """Return the prompt for the API."""
316  if not exposed_entities:
317  return (
318  "Only if the user wants to control a device, tell them to expose entities "
319  "to their voice assistant in Home Assistant."
320  )
321 
322  prompt = [
323  (
324  "When controlling Home Assistant always call the intent tools. "
325  "Use HassTurnOn to lock and HassTurnOff to unlock a lock. "
326  "When controlling a device, prefer passing just name and domain. "
327  "When controlling an area, prefer passing just area name and domain."
328  )
329  ]
330  area: ar.AreaEntry | None = None
331  floor: fr.FloorEntry | None = None
332  if llm_context.device_id:
333  device_reg = dr.async_get(self.hass)
334  device = device_reg.async_get(llm_context.device_id)
335 
336  if device:
337  area_reg = ar.async_get(self.hass)
338  if device.area_id and (area := area_reg.async_get_area(device.area_id)):
339  floor_reg = fr.async_get(self.hass)
340  if area.floor_id:
341  floor = floor_reg.async_get_floor(area.floor_id)
342 
343  extra = "and all generic commands like 'turn on the lights' should target this area."
344 
345  if floor and area:
346  prompt.append(f"You are in area {area.name} (floor {floor.name}) {extra}")
347  elif area:
348  prompt.append(f"You are in area {area.name} {extra}")
349  else:
350  prompt.append(
351  "When a user asks to turn on all devices of a specific type, "
352  "ask user to specify an area, unless there is only one device of that type."
353  )
354 
355  if not llm_context.device_id or not async_device_supports_timers(
356  self.hass, llm_context.device_id
357  ):
358  prompt.append("This device is not able to start timers.")
359 
360  if exposed_entities:
361  prompt.append(
362  "An overview of the areas and the devices in this smart home:"
363  )
364  prompt.append(yaml.dump(list(exposed_entities.values())))
365 
366  return "\n".join(prompt)
367 
368  @callback
370  self, llm_context: LLMContext, exposed_entities: dict | None
371  ) -> list[Tool]:
372  """Return a list of LLM tools."""
373  ignore_intents = self.IGNORE_INTENTSIGNORE_INTENTS
374  if not llm_context.device_id or not async_device_supports_timers(
375  self.hass, llm_context.device_id
376  ):
377  ignore_intents = ignore_intents | {
378  intent.INTENT_START_TIMER,
379  intent.INTENT_CANCEL_TIMER,
380  intent.INTENT_INCREASE_TIMER,
381  intent.INTENT_DECREASE_TIMER,
382  intent.INTENT_PAUSE_TIMER,
383  intent.INTENT_UNPAUSE_TIMER,
384  intent.INTENT_TIMER_STATUS,
385  }
386 
387  intent_handlers = [
388  intent_handler
389  for intent_handler in intent.async_get(self.hass)
390  if intent_handler.intent_type not in ignore_intents
391  ]
392 
393  exposed_domains: set[str] | None = None
394  if exposed_entities is not None:
395  exposed_domains = {
396  split_entity_id(entity_id)[0] for entity_id in exposed_entities
397  }
398  intent_handlers = [
399  intent_handler
400  for intent_handler in intent_handlers
401  if intent_handler.platforms is None
402  or intent_handler.platforms & exposed_domains
403  ]
404 
405  tools: list[Tool] = [
406  IntentTool(self.cached_slugifycached_slugify(intent_handler.intent_type), intent_handler)
407  for intent_handler in intent_handlers
408  ]
409 
410  if llm_context.assistant is not None:
411  for state in self.hass.states.async_all(SCRIPT_DOMAIN):
412  if not async_should_expose(
413  self.hass, llm_context.assistant, state.entity_id
414  ):
415  continue
416 
417  tools.append(ScriptTool(self.hass, state.entity_id))
418 
419  return tools
420 
421 
423  hass: HomeAssistant, assistant: str
424 ) -> dict[str, dict[str, Any]]:
425  """Get exposed entities."""
426  area_registry = ar.async_get(hass)
427  entity_registry = er.async_get(hass)
428  device_registry = dr.async_get(hass)
429  interesting_attributes = {
430  "temperature",
431  "current_temperature",
432  "temperature_unit",
433  "brightness",
434  "humidity",
435  "unit_of_measurement",
436  "device_class",
437  "current_position",
438  "percentage",
439  "volume_level",
440  "media_title",
441  "media_artist",
442  "media_album_name",
443  }
444 
445  entities = {}
446 
447  for state in hass.states.async_all():
448  if (
449  not async_should_expose(hass, assistant, state.entity_id)
450  or state.domain == SCRIPT_DOMAIN
451  ):
452  continue
453 
454  description: str | None = None
455  entity_entry = entity_registry.async_get(state.entity_id)
456  names = [state.name]
457  area_names = []
458 
459  if entity_entry is not None:
460  names.extend(entity_entry.aliases)
461  if entity_entry.area_id and (
462  area := area_registry.async_get_area(entity_entry.area_id)
463  ):
464  # Entity is in area
465  area_names.append(area.name)
466  area_names.extend(area.aliases)
467  elif entity_entry.device_id and (
468  device := device_registry.async_get(entity_entry.device_id)
469  ):
470  # Check device area
471  if device.area_id and (
472  area := area_registry.async_get_area(device.area_id)
473  ):
474  area_names.append(area.name)
475  area_names.extend(area.aliases)
476 
477  info: dict[str, Any] = {
478  "names": ", ".join(names),
479  "domain": state.domain,
480  "state": state.state,
481  }
482 
483  if description:
484  info["description"] = description
485 
486  if area_names:
487  info["areas"] = ", ".join(area_names)
488 
489  if attributes := {
490  attr_name: str(attr_value)
491  if isinstance(attr_value, (Enum, Decimal, int))
492  else attr_value
493  for attr_name, attr_value in state.attributes.items()
494  if attr_name in interesting_attributes
495  }:
496  info["attributes"] = attributes
497 
498  entities[state.entity_id] = info
499 
500  return entities
501 
502 
503 def _selector_serializer(schema: Any) -> Any: # noqa: C901
504  """Convert selectors into OpenAPI schema."""
505  if not isinstance(schema, selector.Selector):
506  return UNSUPPORTED
507 
508  if isinstance(schema, selector.BackupLocationSelector):
509  return {"type": "string", "pattern": "^(?:\\/backup|\\w+)$"}
510 
511  if isinstance(schema, selector.BooleanSelector):
512  return {"type": "boolean"}
513 
514  if isinstance(schema, selector.ColorRGBSelector):
515  return {
516  "type": "array",
517  "items": {"type": "number"},
518  "minItems": 3,
519  "maxItems": 3,
520  "format": "RGB",
521  }
522 
523  if isinstance(schema, selector.ConditionSelector):
524  return convert(cv.CONDITIONS_SCHEMA)
525 
526  if isinstance(schema, selector.ConstantSelector):
527  return convert(vol.Schema(schema.config["value"]))
528 
529  result: dict[str, Any]
530  if isinstance(schema, selector.ColorTempSelector):
531  result = {"type": "number"}
532  if "min" in schema.config:
533  result["minimum"] = schema.config["min"]
534  elif "min_mireds" in schema.config:
535  result["minimum"] = schema.config["min_mireds"]
536  if "max" in schema.config:
537  result["maximum"] = schema.config["max"]
538  elif "max_mireds" in schema.config:
539  result["maximum"] = schema.config["max_mireds"]
540  return result
541 
542  if isinstance(schema, selector.CountrySelector):
543  if schema.config.get("countries"):
544  return {"type": "string", "enum": schema.config["countries"]}
545  return {"type": "string", "format": "ISO 3166-1 alpha-2"}
546 
547  if isinstance(schema, selector.DateSelector):
548  return {"type": "string", "format": "date"}
549 
550  if isinstance(schema, selector.DateTimeSelector):
551  return {"type": "string", "format": "date-time"}
552 
553  if isinstance(schema, selector.DurationSelector):
554  return convert(cv.time_period_dict)
555 
556  if isinstance(schema, selector.EntitySelector):
557  if schema.config.get("multiple"):
558  return {"type": "array", "items": {"type": "string", "format": "entity_id"}}
559 
560  return {"type": "string", "format": "entity_id"}
561 
562  if isinstance(schema, selector.LanguageSelector):
563  if schema.config.get("languages"):
564  return {"type": "string", "enum": schema.config["languages"]}
565  return {"type": "string", "format": "RFC 5646"}
566 
567  if isinstance(schema, (selector.LocationSelector, selector.MediaSelector)):
568  return convert(schema.DATA_SCHEMA)
569 
570  if isinstance(schema, selector.NumberSelector):
571  result = {"type": "number"}
572  if "min" in schema.config:
573  result["minimum"] = schema.config["min"]
574  if "max" in schema.config:
575  result["maximum"] = schema.config["max"]
576  return result
577 
578  if isinstance(schema, selector.ObjectSelector):
579  return {"type": "object", "additionalProperties": True}
580 
581  if isinstance(schema, selector.SelectSelector):
582  options = [
583  x["value"] if isinstance(x, dict) else x for x in schema.config["options"]
584  ]
585  if schema.config.get("multiple"):
586  return {
587  "type": "array",
588  "items": {"type": "string", "enum": options},
589  "uniqueItems": True,
590  }
591  return {"type": "string", "enum": options}
592 
593  if isinstance(schema, selector.TargetSelector):
594  return convert(cv.TARGET_SERVICE_FIELDS)
595 
596  if isinstance(schema, selector.TemplateSelector):
597  return {"type": "string", "format": "jinja2"}
598 
599  if isinstance(schema, selector.TimeSelector):
600  return {"type": "string", "format": "time"}
601 
602  if isinstance(schema, selector.TriggerSelector):
603  return {"type": "array", "items": {"type": "string"}}
604 
605  if schema.config.get("multiple"):
606  return {"type": "array", "items": {"type": "string"}}
607 
608  return {"type": "string"}
609 
610 
612  hass: HomeAssistant, entity_id: str
613 ) -> tuple[str | None, vol.Schema]:
614  """Get script description and schema."""
615  entity_registry = er.async_get(hass)
616 
617  description = None
618  parameters = vol.Schema({})
619  entity_entry = entity_registry.async_get(entity_id)
620  if entity_entry and entity_entry.unique_id:
621  parameters_cache = hass.data.get(SCRIPT_PARAMETERS_CACHE)
622 
623  if parameters_cache is None:
624  parameters_cache = hass.data[SCRIPT_PARAMETERS_CACHE] = {}
625 
626  @callback
627  def clear_cache(event: Event) -> None:
628  """Clear script parameter cache on script reload or delete."""
629  if (
630  event.data[ATTR_DOMAIN] == SCRIPT_DOMAIN
631  and event.data[ATTR_SERVICE] in parameters_cache
632  ):
633  parameters_cache.pop(event.data[ATTR_SERVICE])
634 
635  cancel = hass.bus.async_listen(EVENT_SERVICE_REMOVED, clear_cache)
636 
637  @callback
638  def on_homeassistant_close(event: Event) -> None:
639  """Cleanup."""
640  cancel()
641 
642  hass.bus.async_listen_once(
643  EVENT_HOMEASSISTANT_CLOSE, on_homeassistant_close
644  )
645 
646  if entity_entry.unique_id in parameters_cache:
647  return parameters_cache[entity_entry.unique_id]
648 
649  if service_desc := service.async_get_cached_service_description(
650  hass, SCRIPT_DOMAIN, entity_entry.unique_id
651  ):
652  description = service_desc.get("description")
653  schema: dict[vol.Marker, Any] = {}
654  fields = service_desc.get("fields", {})
655 
656  for field, config in fields.items():
657  field_description = config.get("description")
658  if not field_description:
659  field_description = config.get("name")
660  key: vol.Marker
661  if config.get("required"):
662  key = vol.Required(field, description=field_description)
663  else:
664  key = vol.Optional(field, description=field_description)
665  if "selector" in config:
666  schema[key] = selector.selector(config["selector"])
667  else:
668  schema[key] = cv.string
669 
670  parameters = vol.Schema(schema)
671 
672  aliases: list[str] = []
673  if entity_entry.name:
674  aliases.append(entity_entry.name)
675  if entity_entry.aliases:
676  aliases.extend(entity_entry.aliases)
677  if aliases:
678  if description:
679  description = description + ". Aliases: " + str(list(aliases))
680  else:
681  description = "Aliases: " + str(list(aliases))
682 
683  parameters_cache[entity_entry.unique_id] = (description, parameters)
684 
685  return description, parameters
686 
687 
689  """LLM Tool representing a Script."""
690 
691  def __init__(
692  self,
693  hass: HomeAssistant,
694  script_entity_id: str,
695  ) -> None:
696  """Init the class."""
697  self._object_id_object_id = self.namename = split_entity_id(script_entity_id)[1]
698  if self.namename[0].isdigit():
699  self.namename = "_" + self.namename
700 
701  self.description, self.parametersparameters = _get_cached_script_parameters(
702  hass, script_entity_id
703  )
704 
705  async def async_call(
706  self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext
707  ) -> JsonObjectType:
708  """Run the script."""
709 
710  for field, validator in self.parametersparameters.schema.items():
711  if field not in tool_input.tool_args:
712  continue
713  if isinstance(validator, selector.AreaSelector):
714  area_reg = ar.async_get(hass)
715  if validator.config.get("multiple"):
716  areas: list[ar.AreaEntry] = []
717  for area in tool_input.tool_args[field]:
718  areas.extend(intent.find_areas(area, area_reg))
719  tool_input.tool_args[field] = list({area.id for area in areas})
720  else:
721  area = tool_input.tool_args[field]
722  area = list(intent.find_areas(area, area_reg))[0].id
723  tool_input.tool_args[field] = area
724 
725  elif isinstance(validator, selector.FloorSelector):
726  floor_reg = fr.async_get(hass)
727  if validator.config.get("multiple"):
728  floors: list[fr.FloorEntry] = []
729  for floor in tool_input.tool_args[field]:
730  floors.extend(intent.find_floors(floor, floor_reg))
731  tool_input.tool_args[field] = list(
732  {floor.floor_id for floor in floors}
733  )
734  else:
735  floor = tool_input.tool_args[field]
736  floor = list(intent.find_floors(floor, floor_reg))[0].floor_id
737  tool_input.tool_args[field] = floor
738 
739  result = await hass.services.async_call(
740  SCRIPT_DOMAIN,
741  self._object_id_object_id,
742  tool_input.tool_args,
743  context=llm_context.context,
744  blocking=True,
745  return_response=True,
746  )
747 
748  return {"success": True, "result": result}
JsonObjectType async_call_tool(self, ToolInput tool_input)
Definition: llm.py:165
APIInstance async_get_api_instance(self, LLMContext llm_context)
Definition: llm.py:190
str _async_get_api_prompt(self, LLMContext llm_context, dict|None exposed_entities)
Definition: llm.py:314
APIInstance async_get_api_instance(self, LLMContext llm_context)
Definition: llm.py:294
list[Tool] _async_get_tools(self, LLMContext llm_context, dict|None exposed_entities)
Definition: llm.py:371
None __init__(self, HomeAssistant hass)
Definition: llm.py:283
JsonObjectType async_call(self, HomeAssistant hass, ToolInput tool_input, LLMContext llm_context)
Definition: llm.py:226
None __init__(self, str name, intent.IntentHandler intent_handler)
Definition: llm.py:202
None __init__(self, HomeAssistant hass, str script_entity_id)
Definition: llm.py:695
JsonObjectType async_call(self, HomeAssistant hass, ToolInput tool_input, LLMContext llm_context)
Definition: llm.py:707
JsonObjectType async_call(self, HomeAssistant hass, ToolInput tool_input, LLMContext llm_context)
Definition: llm.py:146
None async_conversation_trace_append(ConversationTraceEventType event_type, dict[str, Any] event_data)
Definition: trace.py:88
bool async_should_expose(HomeAssistant hass, str assistant, str entity_id)
bool async_device_supports_timers(HomeAssistant hass, str device_id)
Definition: timers.py:478
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
str async_render_no_api_prompt(HomeAssistant hass)
Definition: llm.py:70
Any _selector_serializer(Any schema)
Definition: llm.py:503
dict[str, API] _async_get_apis(HomeAssistant hass)
Definition: llm.py:80
tuple[str|None, vol.Schema] _get_cached_script_parameters(HomeAssistant hass, str entity_id)
Definition: llm.py:613
list[API] async_get_apis(HomeAssistant hass)
Definition: llm.py:111
APIInstance async_get_api(HomeAssistant hass, str api_id, LLMContext llm_context)
Definition: llm.py:100
dict[str, dict[str, Any]] _get_exposed_entities(HomeAssistant hass, str assistant)
Definition: llm.py:424
None async_register_api(HomeAssistant hass, API api)
Definition: llm.py:88