From 735c440d71507a4accb84b37454899d0c92e8879 Mon Sep 17 00:00:00 2001 From: Austin Date: Mon, 27 Oct 2025 22:23:14 -0500 Subject: [PATCH 1/9] feat: codex/major-change-002: toolbox parity + web search + MCP (one-shot) --- .vscode/settings.json | 3 + README.md | 56 ++- .../config_flow.py | 127 ++++++- .../extended_openai_conversation/const.py | 123 +++++++ .../conversation.py | 321 ++++++++++++++---- .../manifest.json | 2 +- .../responses_adapter.py | 37 ++ .../extended_openai_conversation/services.py | 28 +- .../extended_openai_conversation/strings.json | 13 +- .../{helpers.py => tools_builtin.py} | 49 ++- .../tools_mcp_bridge.py | 92 +++++ .../tools_orchestrator.py | 302 ++++++++++++++++ .../tools_web_search.py | 112 ++++++ .../translations/en.json | 16 +- docs/PLAN_TOOLBOX.md | 32 ++ 15 files changed, 1238 insertions(+), 75 deletions(-) create mode 100644 .vscode/settings.json rename custom_components/extended_openai_conversation/{helpers.py => tools_builtin.py} (93%) create mode 100644 custom_components/extended_openai_conversation/tools_mcp_bridge.py create mode 100644 custom_components/extended_openai_conversation/tools_orchestrator.py create mode 100644 custom_components/extended_openai_conversation/tools_web_search.py create mode 100644 docs/PLAN_TOOLBOX.md diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..457f44d9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.typeCheckingMode": "basic" +} \ No newline at end of file diff --git a/README.md b/README.md index 6346d623..0288292d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ A maintained fork of **Extended OpenAI Conversation** for **Home Assistant** tha - **Options gear** (model, strategy, effort, max tokens, temp/top‑p, prompt) via `OptionsFlowWithReload` - **Azure OpenAI** support (Base URL + API version) - **Async‑safe client** using HA’s shared HTTPX to avoid blocking SSL CA loads +- **Toolbox parity** with upstream (service execution, automations, history, REST/scrape/script/template/composite when configured) +- **Hosted web search** hook for reasoning models with graceful chat fallback +- **Optional MCP bridge** (lazy import; safe no-op when library absent) +- **Tool execution limits** (per-call timeout, depth/call caps, chat log breadcrumbs for start/result) ## Requirements - Home Assistant **2025.4.0+** recommended @@ -36,9 +40,59 @@ A maintained fork of **Extended OpenAI Conversation** for **Home Assistant** tha Open the integration card → **Configure** (gear): - **Model** (default `gpt-5`) - **Model strategy**: `auto` \| `force_chat_completions` \| `force_responses_api` -- **Use Responses API** (for non‑reasoning models when strategy is `auto`) +- **Use Responses API** (for non-reasoning models when strategy is `auto`) - **Reasoning effort**: `minimal` \| `low` \| `medium` \| `high` - **Max tokens**, **Temperature**, **Top‑p**, **System prompt** +- **Functions YAML**: list of tool specs (defaults cover service/automations/history); extend to add REST/scrape/script/template/composite entries. +- **Tool limits**: max tool calls/iterations, per-call timeout, response size cap. +- **Web search**: enable hosted search, context size (`small`/`medium`/`large` or raw token budget), optional approximate location sharing. +- **MCP bridge**: toggles + timeout/payload caps (requires [`mcp`](https://pypi.org/project/mcp/) to expose external tools). + +### Toolbox +- Default toolbox exposes `execute_service`, `add_automation`, and `get_history` +- Extend via **Functions YAML** in options. Each entry: + +```yaml +- spec: + name: rest_weather + description: Fetch the daily forecast from the weather API. + parameters: + type: object + properties: + city: + type: string + function: + type: rest + resource: https://api.example.com/forecast + method: GET + headers: + Authorization: "{{ secrets.weather_token }}" + payload_template: "{{ {'city': city} | tojson }}" +``` + +- `scrape` tool lazily imports `beautifulsoup4` (installed via manifest); if import fails at runtime the tool returns a friendly error without breaking the integration. +- Tool calls are capped per turn and per loop, with start/result breadcrumbs logged to the Assist chat log for auditability. + +### Web search +- When `Enable hosted web search` is on, reasoning paths send the official `web_search` tool. +- Context size accepts `small`/`medium`/`large` or an integer budget (auto-mapped to low/medium/high). +- Optionally share approximate location (city/region/country/timezone) using HA config data. +- Chat Completions log a notice when the selected model lacks hosted search support and continue without failing. + +### MCP bridge +- Disabled by default; toggle **Enable MCP bridge** to attempt discovery via the optional [`mcp`](https://pypi.org/project/mcp/) SDK. +- When the SDK is unavailable the bridge is a no-op. Future releases can hook into `MCPBridge` for richer behaviour. +- Timeout and payload caps prevent runaway external tool calls. + +## Manual validation checklist +- Base conversation returns `ConversationResult` with language, `response_type`, and `continue_conversation` populated. +- `execute_service` honours Assist exposure; non-exposed entities return a readable denial. +- `get_history` responds with a concise summary (≤10 entries per entity). +- `rest` and `scrape` enforce per-call timeout/output caps; missing `beautifulsoup4` yields a graceful error. +- `composite` chains respect the orchestrator depth limit (safe failure message). +- Web search works end-to-end on reasoning models and logs a skip message on unsupported chat models. +- MCP bridge disabled → no change; when enabled (with SDK) tool discovery/execution routes through the orchestrator. +- Tool start/result breadcrumbs appear in the HA chat log for traceability. ### Recommended (long, smart chats) - Model: `gpt-5` diff --git a/custom_components/extended_openai_conversation/config_flow.py b/custom_components/extended_openai_conversation/config_flow.py index cb1e0dd8..3c8ab447 100644 --- a/custom_components/extended_openai_conversation/config_flow.py +++ b/custom_components/extended_openai_conversation/config_flow.py @@ -5,6 +5,7 @@ from typing import Any import voluptuous as vol +import yaml from homeassistant import config_entries from homeassistant.config_entries import ( ConfigFlow, @@ -26,6 +27,30 @@ CONF_TOP_P, CONF_MAX_TOKENS, CONF_REASONING_EFFORT, + CONF_FUNCTIONS, + CONF_FUNCTIONS_RAW, + CONF_MAX_TOOL_CALLS, + CONF_MAX_TOOL_CHAIN, + CONF_TOOL_TIMEOUT, + CONF_TOOL_MAX_OUTPUT_CHARS, + CONF_ENABLE_WEB_SEARCH, + CONF_WEB_SEARCH_CONTEXT_SIZE, + CONF_INCLUDE_HOME_LOCATION, + CONF_ENABLE_MCP, + CONF_MCP_TIMEOUT, + CONF_MCP_MAX_PAYLOAD, + DEFAULT_FUNCTIONS, + DEFAULT_MAX_TOOL_CALLS, + DEFAULT_MAX_TOOL_CHAIN, + DEFAULT_TOOL_TIMEOUT, + DEFAULT_TOOL_MAX_OUTPUT_CHARS, + DEFAULT_ENABLE_WEB_SEARCH, + DEFAULT_WEB_SEARCH_CONTEXT_SIZE, + DEFAULT_INCLUDE_HOME_LOCATION, + DEFAULT_ENABLE_MCP, + DEFAULT_MCP_TIMEOUT, + DEFAULT_MCP_MAX_PAYLOAD, + WEB_SEARCH_CONTEXT_SIZE_PRESETS, DEFAULT_BASE_URL, DEFAULT_CHAT_MODEL, DEFAULT_MODEL_STRATEGY, @@ -36,6 +61,39 @@ DEFAULT_PROMPT, ) +def _dump_functions_yaml(functions: list[dict[str, Any]] | None) -> str: + if not functions: + return "" + return yaml.safe_dump(functions, sort_keys=False) + + +def _parse_functions_yaml(raw: str) -> list[dict[str, Any]]: + if not raw.strip(): + return [] + + try: + loaded = yaml.safe_load(raw) or [] + except yaml.YAMLError as err: + raise vol.Invalid("invalid_yaml") from err + + if not isinstance(loaded, list): + raise vol.Invalid("invalid_yaml") + + normalized: list[dict[str, Any]] = [] + for item in loaded: + if not isinstance(item, dict): + raise vol.Invalid("invalid_yaml") + + spec = item.get("spec") + runtime = item.get("function") + if not isinstance(spec, dict) or not isinstance(runtime, dict): + raise vol.Invalid("invalid_yaml") + if not spec.get("name") or not runtime.get("type"): + raise vol.Invalid("invalid_yaml") + normalized.append({"spec": spec, "function": runtime}) + + return normalized + class ExtendedOpenAIConfigFlow(ConfigFlow, domain=DOMAIN): """Create the config entry.""" @@ -108,11 +166,47 @@ class EOCOptionsFlow(OptionsFlowWithReload): async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: + errors: dict[str, str] = {} if user_input is not None: - return self.async_create_entry(data=user_input) + raw_functions = user_input.get(CONF_FUNCTIONS_RAW, "") + try: + parsed_functions = _parse_functions_yaml(raw_functions) + except vol.Invalid: + errors[CONF_FUNCTIONS_RAW] = "invalid_functions_yaml" + else: + user_input[CONF_FUNCTIONS] = parsed_functions + user_input[CONF_FUNCTIONS_RAW] = raw_functions.strip() + + if not errors: + return self.async_create_entry(data=user_input) # Build the form with suggested current values. opts = self.config_entry.options + functions_default = opts.get(CONF_FUNCTIONS) + functions_default_raw = opts.get(CONF_FUNCTIONS_RAW) + if functions_default_raw is None: + functions_default_raw = _dump_functions_yaml(functions_default or DEFAULT_FUNCTIONS) + if user_input and CONF_FUNCTIONS_RAW in user_input: + functions_default_raw = user_input.get(CONF_FUNCTIONS_RAW, functions_default_raw) + + max_tool_calls_default = opts.get(CONF_MAX_TOOL_CALLS, DEFAULT_MAX_TOOL_CALLS) + max_tool_chain_default = opts.get(CONF_MAX_TOOL_CHAIN, DEFAULT_MAX_TOOL_CHAIN) + tool_timeout_default = opts.get(CONF_TOOL_TIMEOUT, DEFAULT_TOOL_TIMEOUT) + tool_max_output_default = opts.get( + CONF_TOOL_MAX_OUTPUT_CHARS, DEFAULT_TOOL_MAX_OUTPUT_CHARS + ) + enable_search_default = opts.get( + CONF_ENABLE_WEB_SEARCH, DEFAULT_ENABLE_WEB_SEARCH + ) + search_context_default = opts.get( + CONF_WEB_SEARCH_CONTEXT_SIZE, DEFAULT_WEB_SEARCH_CONTEXT_SIZE + ) + include_location_default = opts.get( + CONF_INCLUDE_HOME_LOCATION, DEFAULT_INCLUDE_HOME_LOCATION + ) + enable_mcp_default = opts.get(CONF_ENABLE_MCP, DEFAULT_ENABLE_MCP) + mcp_timeout_default = opts.get(CONF_MCP_TIMEOUT, DEFAULT_MCP_TIMEOUT) + mcp_payload_default = opts.get(CONF_MCP_MAX_PAYLOAD, DEFAULT_MCP_MAX_PAYLOAD) schema = vol.Schema( { @@ -128,6 +222,35 @@ async def async_step_init( vol.Optional(CONF_TOP_P, default=opts.get(CONF_TOP_P, DEFAULT_TOP_P)): vol.Coerce(float), vol.Optional(CONF_MAX_TOKENS, default=opts.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)): vol.Coerce(int), vol.Optional("prompt", default=opts.get("prompt", DEFAULT_PROMPT)): str, + vol.Optional(CONF_FUNCTIONS_RAW, default=functions_default_raw): str, + vol.Optional( + CONF_MAX_TOOL_CALLS, default=max_tool_calls_default + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=10)), + vol.Optional( + CONF_MAX_TOOL_CHAIN, default=max_tool_chain_default + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=8)), + vol.Optional(CONF_TOOL_TIMEOUT, default=tool_timeout_default): vol.All( + vol.Coerce(float), vol.Range(min=1, max=60) + ), + vol.Optional( + CONF_TOOL_MAX_OUTPUT_CHARS, default=tool_max_output_default + ): vol.All(vol.Coerce(int), vol.Range(min=256, max=16384)), + vol.Optional(CONF_ENABLE_WEB_SEARCH, default=enable_search_default): bool, + vol.Optional( + CONF_WEB_SEARCH_CONTEXT_SIZE, + default=search_context_default, + ): vol.Any( + vol.In(list(WEB_SEARCH_CONTEXT_SIZE_PRESETS.keys())), + vol.All(vol.Coerce(int), vol.Range(min=128, max=4096)), + ), + vol.Optional(CONF_INCLUDE_HOME_LOCATION, default=include_location_default): bool, + vol.Optional(CONF_ENABLE_MCP, default=enable_mcp_default): bool, + vol.Optional(CONF_MCP_TIMEOUT, default=mcp_timeout_default): vol.All( + vol.Coerce(float), vol.Range(min=1, max=60) + ), + vol.Optional( + CONF_MCP_MAX_PAYLOAD, default=mcp_payload_default + ): vol.All(vol.Coerce(int), vol.Range(min=512, max=65536)), } ) - return self.async_show_form(step_id="init", data_schema=schema) + return self.async_show_form(step_id="init", data_schema=schema, errors=errors) diff --git a/custom_components/extended_openai_conversation/const.py b/custom_components/extended_openai_conversation/const.py index 57ed1453..e39be0eb 100644 --- a/custom_components/extended_openai_conversation/const.py +++ b/custom_components/extended_openai_conversation/const.py @@ -30,6 +30,33 @@ # Optional scaffolding (off by default) CONF_MEMORY_ENABLED = "memory_enabled" CONF_MEMORY_DEFAULT_NAMESPACE = "memory_default_namespace" +CONF_MEMORY_BASE_URL = "memory_base_url" +CONF_MEMORY_API_KEY = "memory_api_key" +CONF_MEMORY_WRITE_PATH = "memory_write_path" +CONF_MEMORY_SEARCH_PATH = "memory_search_path" + +# Toolbox / tools +CONF_FUNCTIONS = "functions" +CONF_FUNCTIONS_RAW = "functions_yaml" +CONF_MAX_TOOL_CALLS = "max_tool_calls" +CONF_MAX_TOOL_CHAIN = "max_tool_chain" +CONF_TOOL_TIMEOUT = "tool_timeout" +CONF_TOOL_MAX_OUTPUT_CHARS = "tool_max_output_chars" +CONF_ENABLE_WEB_SEARCH = "enable_web_search" +CONF_WEB_SEARCH_CONTEXT_SIZE = "web_search_context_size" +CONF_INCLUDE_HOME_LOCATION = "include_home_location" +CONF_ENABLE_MCP = "enable_mcp" +CONF_MCP_TIMEOUT = "mcp_timeout" +CONF_MCP_MAX_PAYLOAD = "mcp_max_payload" + +WEB_SEARCH_CONTEXT_SIZE_SMALL = "small" +WEB_SEARCH_CONTEXT_SIZE_MEDIUM = "medium" +WEB_SEARCH_CONTEXT_SIZE_LARGE = "large" +WEB_SEARCH_CONTEXT_SIZE_PRESETS = { + WEB_SEARCH_CONTEXT_SIZE_SMALL: 256, + WEB_SEARCH_CONTEXT_SIZE_MEDIUM: 512, + WEB_SEARCH_CONTEXT_SIZE_LARGE: 1024, +} # Optional service SERVICE_QUERY_IMAGE = "query_image" @@ -47,3 +74,99 @@ DEFAULT_PROMPT = "" DEFAULT_MEMORY_ENABLED = False DEFAULT_MEMORY_DEFAULT_NAMESPACE = "default" +DEFAULT_MEMORY_BASE_URL = None +DEFAULT_MEMORY_API_KEY = None +DEFAULT_MEMORY_WRITE_PATH = "/v1/memory/write" +DEFAULT_MEMORY_SEARCH_PATH = "/v1/memory/search" +DEFAULT_FUNCTIONS: list[dict[str, object]] = [ + { + "spec": { + "name": "execute_service", + "description": "Call one or more Home Assistant services on entities exposed to Assist.", + "parameters": { + "type": "object", + "properties": { + "list": { + "type": "array", + "description": "Service calls to execute in order.", + "items": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Service domain, e.g. light or climate.", + }, + "service": { + "type": "string", + "description": "The service to call within the domain.", + }, + "service_data": { + "type": "object", + "description": "Service data including entity_id for an exposed entity.", + }, + }, + "required": ["domain", "service"], + }, + "minItems": 1, + } + }, + "required": ["list"], + }, + }, + "function": {"type": "native", "name": "execute_service"}, + }, + { + "spec": { + "name": "add_automation", + "description": "Append a YAML automation to automations.yaml and reload automations.", + "parameters": { + "type": "object", + "properties": { + "automation_config": { + "type": "string", + "description": "YAML string describing the automation to add.", + } + }, + "required": ["automation_config"], + }, + }, + "function": {"type": "native", "name": "add_automation"}, + }, + { + "spec": { + "name": "get_history", + "description": "Fetch recent state history for entities exposed to Assist.", + "parameters": { + "type": "object", + "properties": { + "entity_ids": { + "type": "array", + "items": {"type": "string"}, + "description": "Entity IDs to query.", + }, + "start_time": { + "type": "string", + "description": "ISO timestamp for the start of the window.", + }, + "end_time": { + "type": "string", + "description": "ISO timestamp for the end of the window.", + }, + }, + "required": ["entity_ids"], + }, + }, + "function": {"type": "native", "name": "get_history"}, + }, +] +DEFAULT_FUNCTIONS_RAW = "" +DEFAULT_MAX_TOOL_CALLS = 4 +DEFAULT_MAX_TOOL_CHAIN = 3 +DEFAULT_TOOL_TIMEOUT = 12 +DEFAULT_TOOL_MAX_OUTPUT_CHARS = 4000 +DEFAULT_ENABLE_WEB_SEARCH = False +DEFAULT_WEB_SEARCH_CONTEXT_SIZE = WEB_SEARCH_CONTEXT_SIZE_MEDIUM +DEFAULT_INCLUDE_HOME_LOCATION = False +DEFAULT_ENABLE_MCP = False +DEFAULT_MCP_TIMEOUT = 8 +DEFAULT_MCP_MAX_PAYLOAD = 4096 diff --git a/custom_components/extended_openai_conversation/conversation.py b/custom_components/extended_openai_conversation/conversation.py index cd9ee71d..2a7be321 100644 --- a/custom_components/extended_openai_conversation/conversation.py +++ b/custom_components/extended_openai_conversation/conversation.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import asyncio from typing import Any, Optional, Literal from homeassistant.components.conversation import ( @@ -57,7 +58,11 @@ def as_dict(self) -> dict[str, Any]: MODEL_STRATEGY_FORCE_RESPONSES, ) from .model_capabilities import detect_model_capabilities -from .responses_adapter import response_text_from_responses_result +from .responses_adapter import ( + response_text_from_responses_result, + extract_function_calls_from_response, +) +from .tools_orchestrator import ToolsOrchestrator, ToolExecutionError _LOGGER = logging.getLogger(__name__) @@ -127,6 +132,14 @@ async def _async_handle_message( model, caps, strategy, use_responses ) + orchestrator = ToolsOrchestrator( + self.hass, + options=options, + chat_log=chat_log, + agent_id=self.entity_id or DOMAIN, + ) + orchestrator.reset() + client = build_async_client( self.hass, api_key=api_key, @@ -139,67 +152,25 @@ async def _async_handle_message( user_text = user_input.text if use_responses: - # ✅ Correct Responses API schema: - # - System instructions go into "instructions" - # - Input content items use type "input_text" - payload: dict[str, Any] = { - "model": model, - "input": [ - { - "role": "user", - "content": [{"type": "input_text", "text": user_text}], - } - ], - } - if sys_prompt: - payload["instructions"] = sys_prompt - - max_tokens = int(options.get(CONF_MAX_TOKENS) or DEFAULT_MAX_TOKENS) - if max_tokens > 0: - payload["max_output_tokens"] = max_tokens - - if caps.is_reasoning: - effort = options.get(CONF_REASONING_EFFORT) - # OpenAI 1.x expects nested object: {"reasoning": {"effort": "low|medium|high|minimal"}} - payload["reasoning"] = {"effort": effort} - try: - result = await client.responses.create(**payload) # type: ignore[arg-type] - text = response_text_from_responses_result(result) - cont = _should_continue(text) - return _ok( - text=text, - language=user_input.language, - conversation_id=user_input.conversation_id, - cont=cont, + responses_result = await self._run_responses_flow( + client=client, + model=model, + sys_prompt=sys_prompt, + user_text=user_text, + options=options, + caps=caps, + user_input=user_input, + orchestrator=orchestrator, ) - except Exception as err: + except ToolExecutionError as err: + _LOGGER.warning("Responses tool failure: %s", err) + return _err(str(err), user_input.language) + except Exception as err: # pragma: no cover - defensive guard _LOGGER.exception("Responses API failure: %s", err) return _err(str(err), user_input.language) - # Fallback: Chat Completions - messages = [] - if sys_prompt: - messages.append({"role": "system", "content": sys_prompt}) - messages.append({"role": "user", "content": user_text}) - - kwargs: dict[str, Any] = {"model": model, "messages": messages} - - max_tokens = int(options.get(CONF_MAX_TOKENS) or DEFAULT_MAX_TOKENS) - if max_tokens > 0: - # Some reasoning-capable chat models use max_completion_tokens - kwargs["max_completion_tokens" if caps.is_reasoning else "max_tokens"] = max_tokens - - if caps.accepts_temperature: - if (t := options.get(CONF_TEMPERATURE)) is not None: - kwargs["temperature"] = float(t) - if (p := options.get(CONF_TOP_P)) is not None: - kwargs["top_p"] = float(p) - - try: - result = await client.chat.completions.create(**kwargs) # type: ignore[arg-type] - msg = result.choices[0].message - text = msg.content or "" + text = response_text_from_responses_result(responses_result) cont = _should_continue(text) return _ok( text=text, @@ -207,10 +178,35 @@ async def _async_handle_message( conversation_id=user_input.conversation_id, cont=cont, ) - except Exception as err: + + orchestrator.configure_chat_web_search(model) + + try: + text = await self._run_chat_flow( + client=client, + model=model, + sys_prompt=sys_prompt, + user_text=user_text, + options=options, + caps=caps, + user_input=user_input, + orchestrator=orchestrator, + ) + except ToolExecutionError as err: + _LOGGER.warning("Chat tool failure: %s", err) + return _err(str(err), user_input.language) + except Exception as err: # pragma: no cover _LOGGER.exception("Chat Completions failure: %s", err) return _err(str(err), user_input.language) + cont = _should_continue(text) + return _ok( + text=text, + language=user_input.language, + conversation_id=user_input.conversation_id, + cont=cont, + ) + def _default_options(self) -> dict[str, Any]: return { CONF_CHAT_MODEL: self.entry.options.get(CONF_CHAT_MODEL) @@ -225,6 +221,215 @@ def _default_options(self) -> dict[str, Any]: "prompt": self.entry.options.get("prompt", DEFAULT_PROMPT), } + async def _run_responses_flow( + self, + *, + client, + model: str, + sys_prompt: str, + user_text: str, + options: dict[str, Any], + caps, + user_input, + orchestrator: ToolsOrchestrator, + ): + payload: dict[str, Any] = { + "model": model, + "input": [ + { + "role": "user", + "content": [{"type": "input_text", "text": user_text}], + } + ], + } + + if sys_prompt: + payload["instructions"] = sys_prompt + + max_tokens = int(options.get(CONF_MAX_TOKENS) or DEFAULT_MAX_TOKENS) + if max_tokens > 0: + payload["max_output_tokens"] = max_tokens + + if caps.is_reasoning: + effort = options.get(CONF_REASONING_EFFORT) + payload["reasoning"] = {"effort": effort} + + tools = orchestrator.conversation_tools_for_responses(self.hass) + if tools: + payload["tools"] = tools + + result = await client.responses.create(**payload) + return await self._handle_responses_tool_calls( + client=client, + model=model, + result=result, + orchestrator=orchestrator, + user_input=user_input, + ) + + async def _handle_responses_tool_calls( + self, + *, + client, + model: str, + result, + orchestrator: ToolsOrchestrator, + user_input, + ): + depth = 0 + + while True: + calls = extract_function_calls_from_response(result) + if not calls: + return result + + outputs: list[dict[str, Any]] = [] + if depth >= orchestrator.max_chain_depth: + for idx, call in enumerate(calls): + call_id = call.get("call_id") or call.get("id") or str(idx) + outputs.append( + { + "type": "function_call_output", + "call_id": call_id, + "output": "Tool execution halted: maximum depth reached.", + } + ) + result = await client.responses.create( + model=model, + previous_response_id=getattr(result, "id", None), + input=outputs, + ) + return result + + for call in calls: + name = call.get("name") + arguments = call.get("arguments") + call_id = call.get("call_id") or call.get("id") or name + if not name: + continue + try: + output = await orchestrator.execute_tool_call( + name=name, + arguments=arguments, + user_input=user_input, + call_id=call_id, + ) + except ToolExecutionError as err: + output = f"Tool {name} failed: {err}" + + outputs.append( + { + "type": "function_call_output", + "call_id": call_id or (name or "unknown"), + "output": output, + } + ) + + if not outputs: + return result + + result = await client.responses.create( + model=model, + previous_response_id=getattr(result, "id", None), + input=outputs, + ) + depth += 1 + + async def _run_chat_flow( + self, + *, + client, + model: str, + sys_prompt: str, + user_text: str, + options: dict[str, Any], + caps, + user_input, + orchestrator: ToolsOrchestrator, + ) -> str: + messages: list[dict[str, Any]] = [] + if sys_prompt: + messages.append({"role": "system", "content": sys_prompt}) + messages.append({"role": "user", "content": user_text}) + + kwargs: dict[str, Any] = {"model": model, "messages": messages} + + max_tokens = int(options.get(CONF_MAX_TOKENS) or DEFAULT_MAX_TOKENS) + if max_tokens > 0: + key = "max_completion_tokens" if caps.is_reasoning else "max_tokens" + kwargs[key] = max_tokens + + if caps.accepts_temperature: + if (t := options.get(CONF_TEMPERATURE)) is not None: + kwargs["temperature"] = float(t) + if (p := options.get(CONF_TOP_P)) is not None: + kwargs["top_p"] = float(p) + + tools = orchestrator.conversation_tools_for_chat() + if tools: + kwargs["tools"] = tools + + depth = 0 + while True: + completion = await client.chat.completions.create(**kwargs) + message = completion.choices[0].message + content = message.content or "" + tool_calls = getattr(message, "tool_calls", None) or [] + + if not tool_calls: + return content + + if depth >= orchestrator.max_chain_depth: + suffix = "Tool chain limit reached; unable to continue." + return f"{content}\n{suffix}" if content else suffix + + assistant_entry = { + "role": "assistant", + "content": content, + "tool_calls": [ + call.model_dump() if hasattr(call, "model_dump") else { + "id": getattr(call, "id", None), + "type": getattr(call, "type", "function"), + "function": { + "name": getattr(getattr(call, "function", None), "name", None), + "arguments": getattr( + getattr(call, "function", None), "arguments", "{}" + ), + }, + } + for call in tool_calls + ], + } + messages.append(assistant_entry) + + for call in tool_calls: + call_id = getattr(call, "id", None) + func = getattr(call, "function", None) + call_name = getattr(func, "name", None) + call_arguments = getattr(func, "arguments", "{}") + if not call_name: + continue + try: + output = await orchestrator.execute_tool_call( + name=call_name, + arguments=call_arguments, + user_input=user_input, + call_id=call_id, + ) + except ToolExecutionError as err: + output = f"Tool {call_name} failed: {err}" + + messages.append( + { + "role": "tool", + "tool_call_id": call_id, + "content": output, + } + ) + + kwargs["messages"] = messages + depth += 1 + def _ok(*, text: str, language: Optional[str], conversation_id: Optional[str], cont: bool) -> ConversationResult: response = intent.IntentResponse(language=language) diff --git a/custom_components/extended_openai_conversation/manifest.json b/custom_components/extended_openai_conversation/manifest.json index 5cb59e3f..fe44148d 100644 --- a/custom_components/extended_openai_conversation/manifest.json +++ b/custom_components/extended_openai_conversation/manifest.json @@ -8,7 +8,7 @@ "iot_class": "cloud_polling", "integration_type": "service", "dependencies": ["conversation"], - "requirements": ["openai>=1.0.0,<2.0.0"], + "requirements": ["openai>=1.0.0,<2.0.0", "beautifulsoup4>=4.12.0"], "version": "1.4.1", "loggers": ["custom_components.extended_openai_conversation"], "platforms": ["conversation"] diff --git a/custom_components/extended_openai_conversation/responses_adapter.py b/custom_components/extended_openai_conversation/responses_adapter.py index 00f7d858..08635171 100644 --- a/custom_components/extended_openai_conversation/responses_adapter.py +++ b/custom_components/extended_openai_conversation/responses_adapter.py @@ -53,3 +53,40 @@ def response_text_from_responses_result(result: Any) -> str: pass return "" + + +def extract_function_calls_from_response(result: Any) -> list[dict[str, Any]]: + """Return function-call payloads from a Responses API result.""" + + try: + data = result.model_dump() + except AttributeError: + data = result + + if not isinstance(data, dict): + return [] + + calls: list[dict[str, Any]] = [] + + def _maybe_add(item: Any) -> None: + if not item: + return + if hasattr(item, "model_dump"): + item = item.model_dump() + if not isinstance(item, dict): + return + if item.get("type") == "function_call": + calls.append(item) + + for entry in data.get("output", []): + _maybe_add(entry) + if not isinstance(entry, dict) and not hasattr(entry, "model_dump"): + continue + if hasattr(entry, "model_dump"): + entry = entry.model_dump() + if not isinstance(entry, dict): + continue + for part in entry.get("content", []): + _maybe_add(part) + + return calls diff --git a/custom_components/extended_openai_conversation/services.py b/custom_components/extended_openai_conversation/services.py index 1a09a6bb..80bb114d 100644 --- a/custom_components/extended_openai_conversation/services.py +++ b/custom_components/extended_openai_conversation/services.py @@ -4,7 +4,6 @@ from pathlib import Path from urllib.parse import urlparse -from openai import AsyncOpenAI from openai._exceptions import OpenAIError from openai.types.chat.chat_completion_content_part_image_param import ( ChatCompletionContentPartImageParam, @@ -21,7 +20,15 @@ from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, SERVICE_QUERY_IMAGE +from .const import ( + DOMAIN, + SERVICE_QUERY_IMAGE, + CONF_API_KEY, + CONF_BASE_URL, + CONF_API_VERSION, + CONF_ORGANIZATION, +) +from .openai_support import build_async_client QUERY_IMAGE_SCHEMA = vol.Schema( { @@ -59,10 +66,21 @@ async def query_image(call: ServiceCall) -> ServiceResponse: } ] _LOGGER.debug("Prompt for %s: %s", model, messages) + entry_id = call.data["config_entry"] + entry = hass.config_entries.async_get_entry(entry_id) + if entry is None: + raise HomeAssistantError(f"Config entry {entry_id} not found") + + data = entry.data + client = build_async_client( + hass, + api_key=data[CONF_API_KEY], + base_url=data.get(CONF_BASE_URL), + api_version=data.get(CONF_API_VERSION), + organization=data.get(CONF_ORGANIZATION), + ) - response = await AsyncOpenAI( - api_key=hass.data[DOMAIN][call.data["config_entry"]]["api_key"] - ).chat.completions.create( + response = await client.chat.completions.create( model=model, messages=messages, max_tokens=call.data["max_tokens"], diff --git a/custom_components/extended_openai_conversation/strings.json b/custom_components/extended_openai_conversation/strings.json index e37f722f..63125971 100644 --- a/custom_components/extended_openai_conversation/strings.json +++ b/custom_components/extended_openai_conversation/strings.json @@ -39,7 +39,18 @@ "temperature": "Temperature (non‑reasoning only)", "top_p": "Top‑p (non‑reasoning only)", "max_tokens": "Max output tokens", - "prompt": "System prompt (optional)" + "prompt": "System prompt (optional)", + "functions_yaml": "Functions YAML", + "max_tool_calls": "Max tool calls per turn", + "max_tool_chain": "Max tool iterations", + "tool_timeout": "Tool timeout (seconds)", + "tool_max_output_chars": "Tool output cap (characters)", + "enable_web_search": "Enable hosted web search", + "web_search_context_size": "Web search context size", + "include_home_location": "Share approximate Home Assistant location", + "enable_mcp": "Enable MCP bridge", + "mcp_timeout": "MCP tool timeout (seconds)", + "mcp_max_payload": "MCP payload cap (bytes)" } } } diff --git a/custom_components/extended_openai_conversation/helpers.py b/custom_components/extended_openai_conversation/tools_builtin.py similarity index 93% rename from custom_components/extended_openai_conversation/helpers.py rename to custom_components/extended_openai_conversation/tools_builtin.py index b39f67f3..c0179e26 100644 --- a/custom_components/extended_openai_conversation/helpers.py +++ b/custom_components/extended_openai_conversation/tools_builtin.py @@ -6,11 +6,9 @@ import re import sqlite3 import time -from typing import Any +from typing import Any, TYPE_CHECKING from urllib import parse -from bs4 import BeautifulSoup - try: from openai import AsyncAzureOpenAI, AsyncOpenAI except Exception: # pragma: no cover @@ -19,6 +17,9 @@ import voluptuous as vol import yaml +if TYPE_CHECKING: + from bs4 import BeautifulSoup + from homeassistant.components import ( automation, conversation, @@ -65,6 +66,18 @@ AZURE_DOMAIN_PATTERN = r"\.(openai\.azure\.com|azure-api\.net)" +HISTORY_SUMMARY_LIMIT = 10 + + +def _lazy_import_bs4(): + try: + from bs4 import BeautifulSoup # type: ignore + except ImportError as exc: # pragma: no cover - optional dep + raise HomeAssistantError( + "beautifulsoup4 is required for the scrape tool but is not installed." + ) from exc + return BeautifulSoup + def get_function_executor(value: str): function_executor = FUNCTION_EXECUTORS.get(value) @@ -382,7 +395,30 @@ async def get_history( no_attributes, ) - return [[self.as_dict(item) for item in sublist] for sublist in result.values()] + summary: list[dict[str, Any]] = [] + for entity_id, states in result.items(): + samples: list[dict[str, Any]] = [] + trimmed = list(states)[-HISTORY_SUMMARY_LIMIT:] + for state in trimmed: + if isinstance(state, State): + samples.append( + { + "last_changed": state.last_changed.isoformat(), + "state": state.state, + "attributes": None if no_attributes else dict(state.attributes), + } + ) + elif isinstance(state, dict): + samples.append( + { + "last_changed": state.get("last_changed"), + "state": state.get("state"), + "attributes": None if no_attributes else state.get("attributes"), + } + ) + summary.append({"entity_id": entity_id, "samples": samples}) + + return summary async def get_energy( self, @@ -554,6 +590,7 @@ async def execute( user_input: conversation.ConversationInput, exposed_entities, ): + _lazy_import_bs4() config = function rest_data = _get_rest_data(hass, config, arguments) coordinator = scrape.coordinator.ScrapeCoordinator( @@ -586,7 +623,7 @@ async def execute( def _async_update_from_rest_data( self, - data: BeautifulSoup, + data: Any, sensor_config: dict[str, Any], arguments: dict[str, Any], ) -> None: @@ -601,7 +638,7 @@ def _async_update_from_rest_data( return value - def _extract_value(self, data: BeautifulSoup, sensor_config: dict[str, Any]) -> Any: + def _extract_value(self, data: Any, sensor_config: dict[str, Any]) -> Any: """Parse the html extraction in the executor.""" value: str | list[str] | None select = sensor_config[scrape.const.CONF_SELECT] diff --git a/custom_components/extended_openai_conversation/tools_mcp_bridge.py b/custom_components/extended_openai_conversation/tools_mcp_bridge.py new file mode 100644 index 00000000..3311daee --- /dev/null +++ b/custom_components/extended_openai_conversation/tools_mcp_bridge.py @@ -0,0 +1,92 @@ +"""Optional MCP bridge for exposing external tools.""" + +from __future__ import annotations + +import importlib +import logging +from typing import Any, Iterable + +from .const import ( + CONF_ENABLE_MCP, + CONF_MCP_MAX_PAYLOAD, + CONF_MCP_TIMEOUT, + DEFAULT_ENABLE_MCP, + DEFAULT_MCP_MAX_PAYLOAD, + DEFAULT_MCP_TIMEOUT, +) + +LOGGER = logging.getLogger(__name__) + + +class MCPBridge: + """Lazy MCP client facade.""" + + def __init__(self, options: dict[str, Any]) -> None: + self._enabled = options.get(CONF_ENABLE_MCP, DEFAULT_ENABLE_MCP) + self._timeout = options.get(CONF_MCP_TIMEOUT, DEFAULT_MCP_TIMEOUT) + self._payload_cap = options.get(CONF_MCP_MAX_PAYLOAD, DEFAULT_MCP_MAX_PAYLOAD) + self._client_module = None + self._load_error: str | None = None + + if not self._enabled: + return + + try: + self._client_module = importlib.import_module("mcp") + except Exception as err: # pragma: no cover - optional dependency + self._load_error = str(err) + LOGGER.debug("MCP bridge disabled: %s", err) + self._enabled = False + + @property + def available(self) -> bool: + return self._enabled and self._client_module is not None + + @property + def payload_cap(self) -> int: + return self._payload_cap + + @property + def timeout(self) -> float: + return self._timeout + + def describe_tools(self) -> list[dict[str, Any]]: + """Return MCP tool specs ready for registration.""" + + if not self.available: + return [] + + # Full MCP discovery is not yet implemented. This placeholder keeps + # the integration safe until the optional dependency is configured. + LOGGER.info( + "MCP support is enabled but automatic MCP tool discovery is not " + "implemented in this build. No MCP tools will be exposed." + ) + return [] + + async def async_call_tool( + self, + name: str, + arguments: dict[str, Any], + ) -> dict[str, Any]: + """Execute an MCP tool call. Currently returns informative error.""" + + if not self.available: + return { + "error": "mcp_unavailable", + "message": self._load_error + or "MCP client library not installed. Install the 'mcp' package to enable MCP tools.", + } + + return { + "error": "mcp_not_implemented", + "message": ( + "MCP support is enabled but runtime execution is not yet implemented." + ), + } + + +def create_bridge(options: dict[str, Any]) -> MCPBridge: + """Factory used by the orchestrator.""" + + return MCPBridge(options) diff --git a/custom_components/extended_openai_conversation/tools_orchestrator.py b/custom_components/extended_openai_conversation/tools_orchestrator.py new file mode 100644 index 00000000..62c662b4 --- /dev/null +++ b/custom_components/extended_openai_conversation/tools_orchestrator.py @@ -0,0 +1,302 @@ +"""Orchestrates tool execution for both Chat Completions and Responses API.""" + +from __future__ import annotations + +import asyncio +import copy +import json +import logging +import time +from typing import Any + +from homeassistant.components.conversation.chat_log import AssistantContent, ChatLog +from homeassistant.components.homeassistant.exposed_entities import async_should_expose +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from .const import ( + CONF_FUNCTIONS, + CONF_MAX_TOOL_CALLS, + CONF_MAX_TOOL_CHAIN, + CONF_TOOL_TIMEOUT, + CONF_TOOL_MAX_OUTPUT_CHARS, + CONF_MEMORY_ENABLED, + DEFAULT_FUNCTIONS, + DEFAULT_MAX_TOOL_CALLS, + DEFAULT_MAX_TOOL_CHAIN, + DEFAULT_TOOL_TIMEOUT, + DEFAULT_TOOL_MAX_OUTPUT_CHARS, + DEFAULT_MEMORY_ENABLED, +) +from .exceptions import InvalidFunction, FunctionNotFound, NativeNotFound +from .memory_tools import ( + MEMORY_TOOL_SPECS, + dispatch_memory_tool, + get_memory_service_config, +) +from .tools_builtin import FUNCTION_EXECUTORS, get_function_executor +from .tools_mcp_bridge import MCPBridge, create_bridge +from .tools_web_search import ( + build_responses_web_search_tool, + configure_chat_completion_web_search, +) + +LOGGER = logging.getLogger(__name__) + +class ToolExecutionError(Exception): + """Raised when a tool fails to execute cleanly.""" + + +class ToolsOrchestrator: + """Coordinate execution of built-in, memory, and optional MCP tools.""" + + def __init__( + self, + hass: HomeAssistant, + *, + options: dict[str, Any], + chat_log: ChatLog | None, + agent_id: str, + ) -> None: + self.hass = hass + self.options = options + self.chat_log = chat_log + self.agent_id = agent_id + + self._max_calls: int = options.get(CONF_MAX_TOOL_CALLS, DEFAULT_MAX_TOOL_CALLS) + self._max_chain_depth: int = options.get( + CONF_MAX_TOOL_CHAIN, DEFAULT_MAX_TOOL_CHAIN + ) + self._call_timeout: float = float( + options.get(CONF_TOOL_TIMEOUT, DEFAULT_TOOL_TIMEOUT) + ) + self._result_cap: int = int( + options.get(CONF_TOOL_MAX_OUTPUT_CHARS, DEFAULT_TOOL_MAX_OUTPUT_CHARS) + ) + + self._call_count = 0 + self._chain_depth = 0 + self._started = time.monotonic() + self._exposed_entities: list[dict[str, Any]] | None = None + + self._tool_specs: list[dict[str, Any]] = [] + self._runtime_map: dict[str, dict[str, Any]] = {} + + self._load_builtin_functions(options.get(CONF_FUNCTIONS, DEFAULT_FUNCTIONS)) + self._memory_enabled = options.get(CONF_MEMORY_ENABLED, DEFAULT_MEMORY_ENABLED) + self._memory_config = ( + get_memory_service_config(options) if self._memory_enabled else None + ) + if self._memory_enabled: + self._register_memory_tools() + + self._mcp_bridge: MCPBridge = create_bridge(options) + if self._mcp_bridge.available: + self._register_mcp_tools() + + # --------------------------------------------------------------------- + # Tool specification helpers + # --------------------------------------------------------------------- + def _load_builtin_functions(self, functions: list[dict[str, Any]] | None) -> None: + entries: list[dict[str, Any]] = copy.deepcopy(DEFAULT_FUNCTIONS) + if functions: + entries.extend(functions) + + for entry in entries: + if not isinstance(entry, dict): + continue + spec = entry.get("spec") + runtime = entry.get("function") + if not isinstance(spec, dict) or not isinstance(runtime, dict): + continue + name = spec.get("name") + if not name: + continue + self._tool_specs = [ + existing for existing in self._tool_specs if existing.get("name") != name + ] + self._tool_specs.append(dict(spec)) + self._runtime_map[name] = dict(runtime) + + def _register_memory_tools(self) -> None: + for spec in MEMORY_TOOL_SPECS: + name = spec.get("name") + if not name: + continue + self._tool_specs.append(dict(spec)) + self._runtime_map[name] = {"type": "memory", "name": name} + + def _register_mcp_tools(self) -> None: + for spec in self._mcp_bridge.describe_tools(): + name = spec.get("name") + if not name: + continue + self._tool_specs.append(spec) + self._runtime_map[name] = {"type": "mcp", "name": name} + + # ------------------------------------------------------------------ + # Tool definitions exposed to OpenAI APIs + # ------------------------------------------------------------------ + def conversation_tools_for_chat(self) -> list[dict[str, Any]]: + return [ + {"type": "function", "function": dict(spec)} for spec in self._tool_specs + ] + + def conversation_tools_for_responses( + self, hass: HomeAssistant + ) -> list[dict[str, Any]]: + tools = [ + {"type": "function", "function": dict(spec)} + for spec in self._tool_specs + ] + if web_search := build_responses_web_search_tool(hass, self.options): + tools.append(web_search) + return tools + + def configure_chat_web_search(self, model: str | None) -> None: + configure_chat_completion_web_search(options=self.options, model=model) + + # ------------------------------------------------------------------ + # Execution helpers + # ------------------------------------------------------------------ + def reset(self) -> None: + self._call_count = 0 + self._chain_depth = 0 + self._started = time.monotonic() + + @property + def max_chain_depth(self) -> int: + return self._max_chain_depth + + def _log_tool_event(self, message: str) -> None: + if not self.chat_log: + return + self.chat_log.async_add_assistant_content_without_tools( + AssistantContent(agent_id=self.agent_id, content=message) + ) + + def _ensure_call_budget(self) -> None: + if self._max_calls and self._call_count >= self._max_calls: + raise ToolExecutionError("Maximum tool calls exceeded") + + def _get_exposed_entities(self) -> list[dict[str, Any]]: + if self._exposed_entities is not None: + return self._exposed_entities + + exposed: list[dict[str, Any]] = [] + for state in self.hass.states.async_all(): + if async_should_expose(self.hass, "conversation", state.entity_id): + exposed.append( + { + "entity_id": state.entity_id, + "name": state.name, + "state": state.state, + } + ) + self._exposed_entities = exposed + return exposed + + def _stringify_result(self, result: Any) -> str: + if isinstance(result, str): + text = result + else: + try: + text = json.dumps(result, default=str, ensure_ascii=False) + except TypeError: + text = str(result) + + text = text.strip() + if self._result_cap and len(text) > self._result_cap: + truncated = text[: self._result_cap - 16].rstrip() + text = f"{truncated}... (truncated)" + return text + + async def execute_tool_call( + self, + *, + name: str, + arguments: str | dict[str, Any] | None, + user_input, + call_id: str | None, + ) -> str: + self._ensure_call_budget() + self._call_count += 1 + runtime = self._runtime_map.get(name) + if runtime is None: + raise ToolExecutionError(f"Unknown tool '{name}'") + + try: + payload = self._prepare_arguments(runtime, arguments) + except (json.JSONDecodeError, InvalidFunction) as err: + raise ToolExecutionError(f"Invalid arguments for {name}: {err}") from err + + self._log_tool_event(f"[tool:start] {name}") + + try: + result = await asyncio.wait_for( + self._invoke(runtime, payload, user_input), + timeout=self._call_timeout, + ) + except asyncio.TimeoutError as err: + raise ToolExecutionError(f"Tool {name} timed out") from err + except (HomeAssistantError, ToolExecutionError) as err: + raise ToolExecutionError(str(err)) from err + except Exception as err: # pragma: no cover - defensive guard + raise ToolExecutionError(f"Tool {name} failed: {err}") from err + + text = self._stringify_result(result) + self._log_tool_event(f"[tool:result] {name}: {text}") + return text + + def _prepare_arguments( + self, runtime: dict[str, Any], arguments: str | dict[str, Any] | None + ) -> dict[str, Any]: + if isinstance(arguments, str) and arguments.strip(): + data = json.loads(arguments) + elif isinstance(arguments, dict): + data = dict(arguments) + else: + data = {} + + runtime_type = runtime.get("type") + if runtime_type in {"memory", "mcp"}: + return data + + executor = get_function_executor(runtime_type) + validated = executor.to_arguments({"type": runtime_type, **data}) + validated.pop("type", None) + return validated + + async def _invoke( + self, runtime: dict[str, Any], arguments: dict[str, Any], user_input + ) -> Any: + runtime_type = runtime.get("type") + if runtime_type == "memory": + if not self._memory_config: + raise ToolExecutionError("Memory service not configured") + name = runtime.get("name") + return await dispatch_memory_tool( + self.hass, self._memory_config, name, arguments + ) + + if runtime_type == "mcp": + return await self._mcp_bridge.async_call_tool( + runtime.get("name"), arguments + ) + + executor = FUNCTION_EXECUTORS.get(runtime_type) + if executor is None: + raise ToolExecutionError(f"Unsupported tool type {runtime_type}") + + try: + return await executor.execute( + self.hass, + runtime, + arguments, + user_input, + self._get_exposed_entities(), + ) + except NativeNotFound as err: + raise ToolExecutionError(str(err)) from err + except FunctionNotFound as err: + raise ToolExecutionError(str(err)) from err diff --git a/custom_components/extended_openai_conversation/tools_web_search.py b/custom_components/extended_openai_conversation/tools_web_search.py new file mode 100644 index 00000000..33f19d3b --- /dev/null +++ b/custom_components/extended_openai_conversation/tools_web_search.py @@ -0,0 +1,112 @@ +"""Hosted web search helpers for Extended OpenAI Conversation.""" + +from __future__ import annotations + +import logging +from typing import Any, Dict + +from homeassistant.core import HomeAssistant + +from .const import ( + CONF_ENABLE_WEB_SEARCH, + CONF_WEB_SEARCH_CONTEXT_SIZE, + CONF_INCLUDE_HOME_LOCATION, + DEFAULT_ENABLE_WEB_SEARCH, + DEFAULT_WEB_SEARCH_CONTEXT_SIZE, +) + +LOGGER = logging.getLogger(__name__) + +_CONTEXT_ALIASES = { + "small": "low", + "low": "low", + "medium": "medium", + "med": "medium", + "mid": "medium", + "large": "high", + "high": "high", +} + + +def _normalize_context_size(value: Any) -> str: + """Map user option to OpenAI context size token.""" + + if isinstance(value, str): + key = value.strip().lower() + if key in _CONTEXT_ALIASES: + return _CONTEXT_ALIASES[key] + + if isinstance(value, (int, float)): + if value <= 384: + return "low" + if value <= 768: + return "medium" + return "high" + + return "medium" + + +def _build_user_location(hass: HomeAssistant) -> Dict[str, str] | None: + """Return approximate location payload if HA config provides details.""" + + location: Dict[str, str] = {"type": "approximate"} + + city = getattr(hass.config, "location_name", None) + if city and city.lower() not in {"home", "house"}: + location["city"] = city + + region = getattr(hass.config, "state", None) + if region: + location["region"] = region + + country = getattr(hass.config, "country", None) + if country: + location["country"] = country + + timezone = getattr(hass.config, "time_zone", None) + if timezone: + location["timezone"] = timezone + + if len(location) > 1: + return location + return None + + +def build_responses_web_search_tool( + hass: HomeAssistant, options: dict[str, Any] +) -> dict[str, Any] | None: + """Return Responses API tool payload for hosted web search if enabled.""" + + if not options.get(CONF_ENABLE_WEB_SEARCH, DEFAULT_ENABLE_WEB_SEARCH): + return None + + context_size = _normalize_context_size( + options.get(CONF_WEB_SEARCH_CONTEXT_SIZE, DEFAULT_WEB_SEARCH_CONTEXT_SIZE) + ) + tool: dict[str, Any] = { + "type": "web_search", + "search_context_size": context_size, + } + + if options.get(CONF_INCLUDE_HOME_LOCATION): + if location := _build_user_location(hass): + tool["user_location"] = location + + return tool + + +def configure_chat_completion_web_search( + *, + options: dict[str, Any], + model: str | None, +) -> None: + """Chat Completions do not yet expose hosted web search; log and skip.""" + + if not options.get(CONF_ENABLE_WEB_SEARCH, DEFAULT_ENABLE_WEB_SEARCH): + return + + LOGGER.info( + "Hosted web search is enabled but chat model '%s' does not expose the " + "feature via Chat Completions; continuing without search.", + model or "", + ) diff --git a/custom_components/extended_openai_conversation/translations/en.json b/custom_components/extended_openai_conversation/translations/en.json index 56ec6132..3273237e 100644 --- a/custom_components/extended_openai_conversation/translations/en.json +++ b/custom_components/extended_openai_conversation/translations/en.json @@ -38,7 +38,21 @@ "temperature": "Temperature (chat models only)", "top_p": "Top‑p (chat models only)", "max_tokens": "Max tokens", - "prompt": "System prompt" + "prompt": "System prompt", + "functions_yaml": "Functions YAML", + "max_tool_calls": "Max tool calls per turn", + "max_tool_chain": "Max tool iterations", + "tool_timeout": "Tool timeout (seconds)", + "tool_max_output_chars": "Tool output cap (characters)", + "enable_web_search": "Enable hosted web search", + "web_search_context_size": "Web search context size", + "include_home_location": "Share approximate Home Assistant location", + "enable_mcp": "Enable MCP bridge", + "mcp_timeout": "MCP tool timeout (seconds)", + "mcp_max_payload": "MCP payload cap (bytes)" + }, + "error": { + "invalid_functions_yaml": "Functions YAML is invalid. Provide a list of specs with 'spec' and 'function'." } } } diff --git a/docs/PLAN_TOOLBOX.md b/docs/PLAN_TOOLBOX.md new file mode 100644 index 00000000..79c97939 --- /dev/null +++ b/docs/PLAN_TOOLBOX.md @@ -0,0 +1,32 @@ +## Toolbox & Options Parity Plan + +### Scope Highlights +- Build a unified tools orchestrator ensuring identical behavior for chat vs reasoning paths, including depth/time limits and chat-log breadcrumbs. +- Implement built-in tools (`execute_service`, `add_automation`, `get_history`, `rest`, `scrape`, `composite`, `script`, `template`) with entity exposure enforcement, payload caps, and lazy imports (BeautifulSoup optional). +- Re-introduce Functions editor in Options flow with strong YAML validation, plus toggles for hosted web search and MCP bridging when available. +- Extend conversation pipeline to honor tool continuations for Responses API (`function_call`/`function_call_output`) and classic Chat Completions tool loops. +- Add hosted web search adapter with context sizing, location enrichment, and graceful no-op on unsupported chat models/backend. +- Optionally bridge MCP tools (namespaced) via lazy import, honoring timeouts and payload safeguards without impacting baseline when unavailable. + +### Key Files / Modules +- `custom_components/extended_openai_conversation/`: `tools_orchestrator.py`, `tools_builtin.py`, `tools_web_search.py`, `tools_mcp_bridge.py`, `conversation.py`, `responses_adapter.py`, `config_flow.py`, `const.py`, `openai_support.py`, `services.py`, `services.yaml`. +- `manifest.json`, `translations/en.json`, `strings.json` (if legacy strings still referenced), `README.md`, `docs/` (this plan + potential future notes). + +### Risks & Mitigations +- **Recursive tool loops**: enforce configurable depth/time limits and emit safe errors when exceeded. +- **Entity access control drift**: centralize exposure checks using HA permissions helpers; add clear user-facing error strings. +- **YAML validation regressions**: wrap parsing in try/except with schema verification and surface translations for misconfiguration. +- **Optional deps (bs4, MCP)**: lazy import and fall back with descriptive errors; include conditional manifest dependency for BeautifulSoup. +- **Hosted search availability mismatch**: introspect model/backend support; log info-level skip rather than raising. +- **Concurrency/event loop**: ensure all I/O uses async/httpx via HA shared client; offload blocking work with executor where required. + +### Manual Validation Checklist +- Base conversation flow returns `ConversationResult` with language + response_type + `continue_conversation`. +- `execute_service` respects exposed entities and denies others with clear messaging. +- `get_history` returns bounded, summarized results. +- `rest` and `scrape` apply timeouts/size caps; `scrape` gracefully fails when `beautifulsoup4` missing. +- `composite` chains calls, enforces loop depth/time caps. +- Web search succeeds on reasoning model and no-ops with log on unsupported chat path; location payload included when enabled. +- MCP bridge disabled by default; when enabled with server it exposes namespaced tools and respects limits. +- Functions editor rejects malformed YAML without crashing Options flow. +- `query_image` service registered and uses consistent credential sourcing. From 6d8d464c3bc083c020662a95dfebe85438948244 Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 28 Oct 2025 12:36:49 -0500 Subject: [PATCH 2/9] feat(orchestration): agent-aware exposure + chat-log breadcrumbs; gate web search; caps --- .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 2124 bytes .../__pycache__/config_flow.cpython-312.pyc | Bin 0 -> 14841 bytes .../__pycache__/const.cpython-312.pyc | Bin 0 -> 4717 bytes .../context_composer.cpython-312.pyc | Bin 0 -> 3714 bytes .../__pycache__/conversation.cpython-312.pyc | Bin 0 -> 21507 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 0 -> 7249 bytes .../__pycache__/memory_tools.cpython-312.pyc | Bin 0 -> 22409 bytes .../model_capabilities.cpython-312.pyc | Bin 0 -> 1116 bytes .../openai_support.cpython-312.pyc | Bin 0 -> 1659 bytes .../responses_adapter.cpython-312.pyc | Bin 0 -> 3802 bytes .../__pycache__/router.cpython-312.pyc | Bin 0 -> 2544 bytes .../__pycache__/services.cpython-312.pyc | Bin 0 -> 6069 bytes .../__pycache__/tools_builtin.cpython-312.pyc | Bin 0 -> 36755 bytes .../tools_mcp_bridge.cpython-312.pyc | Bin 0 -> 3785 bytes .../tools_orchestrator.cpython-312.pyc | Bin 0 -> 16745 bytes .../tools_web_search.cpython-312.pyc | Bin 0 -> 4260 bytes .../conversation.py | 132 +++++++++++++++++- .../tools_builtin.py | 11 +- .../tools_orchestrator.py | 108 +++++++++++--- .../tools_web_search.py | 26 +++- 20 files changed, 247 insertions(+), 30 deletions(-) create mode 100644 custom_components/extended_openai_conversation/__pycache__/__init__.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/config_flow.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/const.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/context_composer.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/conversation.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/exceptions.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/memory_tools.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/model_capabilities.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/openai_support.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/responses_adapter.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/router.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/services.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/tools_builtin.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/tools_mcp_bridge.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/tools_orchestrator.cpython-312.pyc create mode 100644 custom_components/extended_openai_conversation/__pycache__/tools_web_search.cpython-312.pyc diff --git a/custom_components/extended_openai_conversation/__pycache__/__init__.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ff06c09daf95ac17cc70f8949f76a77922da47f GIT binary patch literal 2124 zcmb_dO=ufO6rRzpWNGzd*|F8wjq8nT2X8S-XbW~iC{Y}j%88|5J2@znWvgA;tL~4O zT{V^Kg5#71L%=PVq=z2TOG62T9D8h_xAtJw2DuA?Kx=4j2KUhVQ2J(9s%%v~6gsdo zZ{Ey%Z{~g9y!}2J&mR=*8{nLEBmz$J(vr+azG2|T#f^|Lk?2Q{5)nF=#?ppr^97UeQ}5e~AuBSUEh$DS%}<)fdkV26TQv=SbSgV3B%f2q z0(QcZVVJhNV!@P285ODpi-Ni^W!QvHqi>nIlCmt-vL(X?MewR7*$_#8jy*SlOWB!J zI+L$w#XU7-4BbWu?_UAoGQyChWmI&Rj&?=n=$y`MYV9@^J!<3{Rq)r$O)U79;u%R- ztdf*hcuTR%rR2Q8IIJw$lEZ;QRti#Cvq=a%2tJ2hlq|~$LF!QqP-cLf#}5mYR7xEs84SR}_<_3S`Zy7e%1hVzoOq)&@(o8 zCw(hjV~tKr-rp53-b<5a}G_(z@ZX?-6M5<5~%jx$}0bbsei0rB> z9!8cEx_Tuw|7Lb}W=`;tD0S8iU7ZW06J)3B5U8Fpc>(kZy6*;b{|>Ei=JCkp=)}q; ziu&C`mF@jRxo>=GRN0XoE>`0Ps@y=0o2YUV-}HVL+Tx~b?DQr(4V2hpkpD71u;^h{ zQ>RjA@RL!TO0rLc0LblWat_|yf29dP^!I~01Q9+A(T6^OybT$O^cA%<+bxV#*?gWw zn%0&E^oq5pU@GIr88D?;o0v`il%&>R`(5oKHZ32~+5`<^_~sChkJO7t*EK^kCD|pP zN4Ja+Bt5XzNq8J6yBow(y~E74y|2-Sp8OI)ZXE_~bjLue~8TZ_-Gq<6fca%bk&%*LtD zhPK#&8hf(Jo_sL!F!@FD@$fb~4uo1iyLNWt`j6ogJHg0$Xf3oJU5joMwu1vZ;hs&l zXD@`h=74XsUabyad=Y<%FZQtyQ?G%T5dz*lC?15uwiJ`qynVy6$rukmhPRynfnrMx zhkeI1wIkapfr+1X^g?71I-%R?5JlO{i=xAcVxbJCiYOAA7AIg=N~*CyC@Y;4i|~Je z67Bt8l1HDl>(%LQ-9i*X&*>a%T`=Id9dyMHen6(|bt045%=M|+xzv0*oAJ=3DR3XS z3&U4vALhu#>L7StH|4UXyg^<89i@>q4&5GuG5!g?x{HQ(QDPUp{uGTpMWef@?^(3B f79FZahc+WGuY~pk436(d{5bwwJb-(N0JeVte4EU$ literal 0 HcmV?d00001 diff --git a/custom_components/extended_openai_conversation/__pycache__/config_flow.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/config_flow.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ded3fc8e93c0ee5ad11b9fd06bdf5f6b4c4b1ec GIT binary patch literal 14841 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 literal 0 HcmV?d00001 diff --git a/custom_components/extended_openai_conversation/__pycache__/context_composer.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/context_composer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f5185f2ff8680160e615810aa5f2de617e9630c3 GIT binary patch literal 3714 zcmaJ^U2GKB6~6O7`{UiUu~~ynGzpIJ64xOrBBUXJZ5;j@gK*LZCup?n%&y0to!Q)( z1-zM+tU?t#ssvGJ?TA#}MptgkovHxqyF$v_1rrd=Bdt`!n6QwyeUqL@D)s{C{N4KCr(Fno<%2-CS6CGe3uR8z1v!6I@;=uKyUOr zy)iAJ#jf+yaV@FE;oA{G-~Xqjv<|RI%=4Mf%GfF0Ea})$TwBT6#ggsl@U>k1s$0&Q ziZk!HdQmB1=yDz9is8-}7TC_|mU5}A6?E4bc}n(}Vf6QM`@I;#8kQT+xhi4n{ImgAmU}i7rrq7e(`kZ08zDUpJ z`1~c?Hhsy(MzN@Clb;K|tg2SFsH>_URn?-cl})0@RrP%Us?{T@s%Gb)IjO2!&`{OS z(FQr?v$0&+aqXf?p|*6(bzX+0bxYGV)h_8))_^YSGF%A7@#WT41on=OM@sWphKZ<1 zfa2@|@iX*ie(*^waqUba+Hvi4Bc8l=w$agf{VkjhZT~mogV6nd5a$71fubvBB)_YL z?l^T5gWPjv{{`JGV@tW5HOsn^2R8{ZR|D5k9X&@qVgy6zAYY;J>abfi2ggSm|^um;Se&T}q_PMhY5N#Ot0|x}5U;>n%4A)l!_r?RH z4}Q*m5DVyQ5yexDWNJN@SbYE1z;fo}%xB{@{`Tp$p<@sGkF9ncUyY5{`B4gPuEk9- z5=aq#6xT6W4sc~OW)8r2SU@qxF}%5|Y7E228 z0z%5fVSs)w8$c1?BHqA7eDr2T7<$inDf4zl!h{j%-TExneZeW222fhI1k9rO{H$$Q z&qzpD)1B)>&2{L8CJm;uGm;}eCblzFf|>>GdEMV=KLr% z1cq{4KjPxDl>ls9`rOq6RDDgJ3rVKj(KK7En_}a^hy^Pe1r{ppopn z`N}PA<=`i?^`uglmA|G3Ywl`#xXyPsr1Z+6N7D0+H2tdK4Ra9Pez% z$s38K#Le=Wyr&`euAF%&4?dK8n=u&EOrXvk%hJcvinN+i{+Jqkm>OJ74K18*#JX?U zHDh(>fqMh>*r7UqCt3Y1ZyiuT5VTO=v9f$6W0J4KH z@pVvs!7igV3wz8}#-Ul0$A%ky55TMPRiP?+JWemkvmv-!j!9s!1_V2a2Vu5n%)1r#Vl=~JlJ3#?z$B1y5=4fho{BT1fe=4L zP}^)68hma}pZDXfj6o&w+aB)6g5-+6n!X(3wlzu?7J?{=K*$vgNV{-le=tb@&RcrK zv~yY0IXbdwI|(~*i1^n6S|57-{)yk7c`){Sadqh2L;1zU_|5!E;c+b8htxUKfD%yZM|<_qbt2)-aGW*)o(bqKe`yZIsQoQrT$z=oX2+FFm>gG zQ2?UwB_JeHuXIi6jstmsGL^$wH#bwtYJSw&)GH}S53r$M29J45FWPwCkKnMS!bYQ$ z85Z}!rg1-rOf*20Fb75dSrj}@@svs1(lLP;$B8&h1TpeOm<+NHpC=Y@L*y~@Mc5gD zM^gou@(p5rl!#-bHxc-x+B$*X*{ZiU?WbBzn>)qCPlA1t6cxV?!XcL*(%bRl!PTX| z=f3bsFg{L36+rwOSZ*g89$7fs*xkQy>L0u)M88fUAyH2}SCj5McYoI!KmM5Ss3%^i zov9xfeQ;!rKTqpQEmQx&8}~n0#;qx!o9h* z*jSw(qgMycOkOHX7RRGR5C}73&`ObR155fVJ#M*V>Oo=#$;q%n65)%PL;&P*G6hAG zY6Q|??h{0#*E6xHy}7B;l}S#PN#bt!kQ6#!CRC!Tm;|&R+x%}qcQBy~tPrt}^oj0# z377?w902bnf&^fY3l0#Cydf0hZRz4O`ACqcAH`?Egpi{{-r*+4FwA3g>MQiZSLpdK zQS3_;`4j4cx2Lbaj?(aDWB+7DrmKlSY;aI*(rOp&;F*CY0@3OqT0&sPn2l5q^J?=z zA2ZH0I?_#!C{I$mn*vc_QcxwTMo|yEEL4r7-o9oBRTC(^yP2eF3W>enbP|=`;(q~) C*^DCq literal 0 HcmV?d00001 diff --git a/custom_components/extended_openai_conversation/__pycache__/conversation.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/conversation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b6cad01111037dc800506e39d92674cf139d4fe GIT binary patch literal 21507 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 O8gF&(e+;jVI{ph5T1DLe literal 0 HcmV?d00001 diff --git a/custom_components/extended_openai_conversation/__pycache__/memory_tools.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/memory_tools.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ff1ebc79971b982193fab66d851778fb12d6bdb GIT binary patch literal 22409 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 literal 0 HcmV?d00001 diff --git a/custom_components/extended_openai_conversation/__pycache__/openai_support.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/openai_support.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24dfe159a8e7a76c2614d54f04bf8c628fd83886 GIT binary patch literal 1659 zcmaJ>O>7%Q6rS;}*Sr47e{oQ(W*QfYCF(Yz5>i`1U4B#))JQ}Tsu5bPz1w)nddHnv zC${BE0rda^i5ub2L$4*=;mQReafw0?+m$%c9Jm>h1IhvMX4Ywv3$xm{-@Nx`=FKsI?5fiflImG(#^K(*`&g|ew{3}9KZ4&se7*D2b^rULF33bU+$3Qw-;X}ZSMN3 zo_J*+5}hyA-sPbz!vHwj=Q*zJ~^2)Oy3@U?*rL=ypB4U3yKwi zmr+T*{cg{(0VZT9kYy6U@uyY7^mZk(2%JGM!IPs@dnzwR(D$+$H1@ph|4Sx|0uqjA%tbd8&yyXKg( z7M6p#$Kd59Fq`OUW+WV)3iZs_#uvtynaxU=&3`+;lP&CKDm!!6e!RCccVkDb>_ajt zDUn{YxMPYkGgiwr#oEAql%;Id^4*@S<2j!~m>Rs2&(8pCq2M`>eY8Eq_mhFxuxy`_ zzQQvBNIY)+#8DQT3}Bw27xxo=1^AZo?kB%bY!5HNG73_svQ&sF$I+77`|blLXfvN! zek{#c1%$G=dqNpQ;gOnEUjs4Ly0JD-i87rg&{qCexx`whuFXl!qxCf*@#W?B$y*iT zQ474T&t2OgLBk==Ivg^%X{#)!?WGqkFI>4efBE9Vd`XQ`g7P(|8;!`LG(ib1>iPi_ z=NV;0T}QH4*(CVbs4(NgoB>l(nIM)4hNKRdIGvIfsSS(s6C=cY$HTbD61yaXWdTML zHF2>|-7}VlfOz?bU^dagG@2|vy7Jq^E1QeY^ywY_{Nrm+t}gy^>&exXXQQ*>)J!;2 z45y2`)2}`1!}q{Qj->}EX`~NEP&OZq<-_q8!!zf?saXhRH3%g&HUe|QCd9AsFn4tA zKjH!VIMSFr)kv*T>Xmf4nMi?9%)vAaku))hBqaisa#6}OZCW?YC~cZ|+aL|cG?|cE zqy^m;92XPXisl-$=>SbG4=g_@Hyp18;x9`v3|W`#Mp}FgCAX65$atqycjQkhW?k^~ z7|Gp8LJ#%pO=`D2=M7ebkhm879+-iGF@B0x_R!oOn%P5d?xC}Tgn|pds~7guD!z#a zFQD0Cc}AC literal 0 HcmV?d00001 diff --git a/custom_components/extended_openai_conversation/__pycache__/responses_adapter.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/responses_adapter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7978ee4119f58aad6336eaa4c9173805c58fd770 GIT binary patch literal 3802 zcma)9U2GFq7QW+|@pxi8juR7-V3KL#{J0HHfpiI!zkrjHLLpmV6@pmr#4`bdV_SE| zB}Nl9(pK6~A=Ri_#nmEhkdT@ceMqaWHtY**+vTMXTQWvCqdZjF?ZdtW$V!NP*>lG~ ziHo=NMmf2^-#Pc3JLfz3*6FN5(7xD#BENJX^iMJ5g#O_n0n17v!qQ?YN3aaMyX7G7NOgPE8<#>x=65o)NU8X*rHJ<%h74cQbMHKHDQ*gB^hN$hieM-)w+q%!F&d*#*E%!?@Zx~SZop$D z_60n)A|6}9vuaj}y3~abp!&`>mCuyoR53Dr8Y@{F z!SpikpJh;rPw{O?HB$k(nGa!_LTAvmZ!e=s`V6{ERa#B)#Z`Zm=!3^wB|v*uR+Bg; zjLC|+O&%YQ#eM0L7C9MUxoql0Zj7G=A@FkMb#v@o!1x3Y?*m)rm6GUMmF3Hi5l$=O(`FqrO zNQ#8jER*kdrn1dxy?4XyxURINiB$j=+xc^Q(TijAs> z5{W6Q7z@h=4T=nw7?dz**kJGjlVLeQa;ISngXQ8_6p)Ek1`~}as^4x{^9dJ|$YYWK z4JM+>6WC^U6Czd(Hep&stB}F%=CmZLqCtb<3K^4_7uxf~;iRI*Cqm))1SEBEuhIke zkYf;@P#p5T7=fi2iHMofdq8_W`G-VFOsFyrBvM9Oan=lRh>%67IHiytFa&M^&BRFM zukh}s(V7Q2>e9c+?3?%L_MYkB3g=GyZqQlIJLmbd>1NX{Z_ZUWV_&nP>P_iiWk%*y z-Pt`oxXd|MEe_tDYiNFgcxPvtS!sG@&Nt65^y*E0OHC&hgLjYIIdboezV}3$Ti)E9 z^LTUKwkL>nccz0Y-nO~me8+-M_wHKq9$oCc+jpn$-k|>G(eyxWvvB!BI`H0@$ zx75CW@z4`wY3$7eb1iLuocv^R-m16sEbP-;4t_HVDjyAxJQyDN?o3D@{!MmFemF3m z>wIl~@TQg(c0Oj|;3^HMkE@ZR`q^41s%dweshp$cd%JU$gW2cG zFvRe0sK0iw1$|ZbI>@h?8X`BbL~fx$e#7APt)*Fe#g!N(`e{ofoWl_)Jj}IspO2=pjNOJ`$eu$Ea!o> z&Z7^3iD%exN}@h+OhJ86kw0K>@f>7(g|Gwgl0_@=3NYKcHoAf~r%FCDvuU(M7tw%4 zyDG2X8sMLwx*z=#jnuW%|Bu1SZUZ@EiZO3fs$3{^%>jI#WYAUiBm&MT)K?~>uu%Cx zB?heWd5JfySN0V6VmuiOlT@{hWGW#crlN6CQu3u@W!4Ck#hKJ}btKMTK&8V49nX@lo3pz<>bu&P9@OnyGVd?h138ELqhnW(WjqfZ z&8swW^gIQ`Jon@;N3yz~F|UG2Pzgtoio#%y1Eu(_<}RbUAG{G6ORDk!#&L|xU4q*P zh!t-p&|V@s2oej7gj1MQK)4-*LOyPjyAJ04|r15a5heE6vvDT$>)b zga+#%iF3B<#0 zone=4wdpsqwnpN;_FJB;tK+lF3(3W3w&!Th;US)WBh#I=cji2ek37By9-r>%m>F2# z4Vz$TcIf&L&_%88=_cuS>r8MN>`)&%ypJIZZ(VhRuGMhUD{7NC?4Tidseoe?YB2p!%oQ9;$jxqFO1pdGvpU4Q##u literal 0 HcmV?d00001 diff --git a/custom_components/extended_openai_conversation/__pycache__/router.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/router.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0211d0d1e7f7563731c162efdfae1279d34e5de3 GIT binary patch literal 2544 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 D%W$>n literal 0 HcmV?d00001 diff --git a/custom_components/extended_openai_conversation/__pycache__/tools_builtin.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/tools_builtin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..644219a3bf7ac4450001766a8271da2e6d74118b GIT binary patch literal 36755 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 literal 0 HcmV?d00001 diff --git a/custom_components/extended_openai_conversation/__pycache__/tools_orchestrator.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/tools_orchestrator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e748b521a3dccc7602b1174644466821b052dda GIT binary patch literal 16745 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 Cj(W2I literal 0 HcmV?d00001 diff --git a/custom_components/extended_openai_conversation/__pycache__/tools_web_search.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/tools_web_search.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9fd144d8678e8000fb7d0b02e1a455f6e31caefa GIT binary patch literal 4260 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 orchestrator.max_chain_depth: + _LOGGER.debug( + "Responses tool loop stopped at depth %s (limit %s)", + depth, + orchestrator.max_chain_depth, + ) for idx, call in enumerate(calls): call_id = call.get("call_id") or call.get("id") or str(idx) outputs.append( @@ -301,6 +327,8 @@ async def _handle_responses_tool_calls( ) return result + self._log_assistant_tool_calls(chat_log, agent_id, None, calls) + for call in calls: name = call.get("name") arguments = call.get("arguments") @@ -346,6 +374,8 @@ async def _run_chat_flow( caps, user_input, orchestrator: ToolsOrchestrator, + chat_log: ChatLog, + agent_id: str, ) -> str: messages: list[dict[str, Any]] = [] if sys_prompt: @@ -381,8 +411,15 @@ async def _run_chat_flow( if depth >= orchestrator.max_chain_depth: suffix = "Tool chain limit reached; unable to continue." + _LOGGER.debug( + "Chat tool loop stopped at depth %s (limit %s)", + depth, + orchestrator.max_chain_depth, + ) return f"{content}\n{suffix}" if content else suffix + self._log_assistant_tool_calls(chat_log, agent_id, content, tool_calls) + assistant_entry = { "role": "assistant", "content": content, @@ -430,6 +467,89 @@ async def _run_chat_flow( kwargs["messages"] = messages depth += 1 + def _log_assistant_tool_calls( + self, + chat_log: ChatLog | None, + agent_id: str, + content: str | None, + tool_calls: list[Any], + ) -> None: + if not chat_log or not tool_calls: + return + + tool_inputs: list[ToolInput] = [] + for call in tool_calls: + call_id, call_name, args = self._normalize_tool_call(call) + if not call_name: + continue + tool_inputs.append( + ToolInput( + tool_name=call_name, + tool_args=args, + id=call_id or call_name, + external=True, + ) + ) + + if not tool_inputs: + return + + chat_log.async_add_assistant_content_without_tools( + AssistantContent( + agent_id=agent_id, + content=content if content else None, + tool_calls=tool_inputs, + ) + ) + + def _normalize_tool_call( + self, call: Any + ) -> tuple[Optional[str], Optional[str], dict[str, Any]]: + call_id: Optional[str] = None + name: Optional[str] = None + raw_args: Any = None + + if hasattr(call, "id") or hasattr(call, "function"): + call_id = getattr(call, "id", None) + func = getattr(call, "function", None) + name = getattr(func, "name", None) + raw_args = getattr(func, "arguments", None) + elif isinstance(call, dict): + call_id = call.get("id") or call.get("call_id") + if (func := call.get("function")) and isinstance(func, dict): + name = func.get("name") + raw_args = func.get("arguments") + else: + name = call.get("name") + raw_args = call.get("arguments") + + return call_id, name, self._parse_tool_arguments(raw_args) + + @staticmethod + def _parse_tool_arguments(raw: Any) -> dict[str, Any]: + if isinstance(raw, dict): + return raw + if isinstance(raw, str): + try: + parsed = json.loads(raw) + except json.JSONDecodeError: + return {"raw": raw} + if isinstance(parsed, dict): + return parsed + return {"value": parsed} + if raw is None: + return {} + return {"value": raw} + + def _log_final_assistant_message( + self, chat_log: ChatLog | None, agent_id: str, text: str + ) -> None: + if not chat_log: + return + chat_log.async_add_assistant_content_without_tools( + AssistantContent(agent_id=agent_id, content=text or "") + ) + def _ok(*, text: str, language: Optional[str], conversation_id: Optional[str], cont: bool) -> ConversationResult: response = intent.IntentResponse(language=language) diff --git a/custom_components/extended_openai_conversation/tools_builtin.py b/custom_components/extended_openai_conversation/tools_builtin.py index c0179e26..fdffb188 100644 --- a/custom_components/extended_openai_conversation/tools_builtin.py +++ b/custom_components/extended_openai_conversation/tools_builtin.py @@ -201,9 +201,14 @@ def to_arguments(self, arguments): def validate_entity_ids(self, hass: HomeAssistant, entity_ids, exposed_entities): if any(hass.states.get(entity_id) is None for entity_id in entity_ids): raise EntityNotFound(entity_ids) - exposed_entity_ids = map(lambda e: e["entity_id"], exposed_entities) - if not set(entity_ids).issubset(exposed_entity_ids): - raise EntityNotExposed(entity_ids) + exposed_entity_ids = {exposed["entity_id"] for exposed in exposed_entities} + missing = set(entity_ids) - exposed_entity_ids + if missing: + _LOGGER.debug( + "Exposure denied for entities %s; not exposed to agent", + sorted(missing), + ) + raise EntityNotExposed(sorted(missing)) @abstractmethod async def execute( diff --git a/custom_components/extended_openai_conversation/tools_orchestrator.py b/custom_components/extended_openai_conversation/tools_orchestrator.py index 62c662b4..8dd0f00c 100644 --- a/custom_components/extended_openai_conversation/tools_orchestrator.py +++ b/custom_components/extended_openai_conversation/tools_orchestrator.py @@ -9,7 +9,7 @@ import time from typing import Any -from homeassistant.components.conversation.chat_log import AssistantContent, ChatLog +from homeassistant.components.conversation.chat_log import ChatLog, ToolResultContent from homeassistant.components.homeassistant.exposed_entities import async_should_expose from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -143,13 +143,15 @@ def conversation_tools_for_chat(self) -> list[dict[str, Any]]: ] def conversation_tools_for_responses( - self, hass: HomeAssistant + self, hass: HomeAssistant, model: str | None ) -> list[dict[str, Any]]: tools = [ {"type": "function", "function": dict(spec)} for spec in self._tool_specs ] - if web_search := build_responses_web_search_tool(hass, self.options): + if web_search := build_responses_web_search_tool( + hass, self.options, model=model + ): tools.append(web_search) return tools @@ -163,29 +165,36 @@ def reset(self) -> None: self._call_count = 0 self._chain_depth = 0 self._started = time.monotonic() + self._exposed_entities = None @property def max_chain_depth(self) -> int: return self._max_chain_depth - def _log_tool_event(self, message: str) -> None: - if not self.chat_log: - return - self.chat_log.async_add_assistant_content_without_tools( - AssistantContent(agent_id=self.agent_id, content=message) - ) - def _ensure_call_budget(self) -> None: if self._max_calls and self._call_count >= self._max_calls: raise ToolExecutionError("Maximum tool calls exceeded") + def _ensure_time_budget(self) -> None: + if self._call_timeout <= 0: + return + max_runtime = self._call_timeout * max(1, self._max_chain_depth) + elapsed = time.monotonic() - self._started + if elapsed > max_runtime: + LOGGER.debug( + "Tool chain time budget exceeded after %.2fs (limit %.2fs)", + elapsed, + max_runtime, + ) + raise ToolExecutionError("Tool chain time budget exceeded") + def _get_exposed_entities(self) -> list[dict[str, Any]]: if self._exposed_entities is not None: return self._exposed_entities exposed: list[dict[str, Any]] = [] for state in self.hass.states.async_all(): - if async_should_expose(self.hass, "conversation", state.entity_id): + if async_should_expose(self.hass, self.agent_id, state.entity_id): exposed.append( { "entity_id": state.entity_id, @@ -220,6 +229,7 @@ async def execute_tool_call( call_id: str | None, ) -> str: self._ensure_call_budget() + self._ensure_time_budget() self._call_count += 1 runtime = self._runtime_map.get(name) if runtime is None: @@ -230,22 +240,59 @@ async def execute_tool_call( except (json.JSONDecodeError, InvalidFunction) as err: raise ToolExecutionError(f"Invalid arguments for {name}: {err}") from err - self._log_tool_event(f"[tool:start] {name}") - + LOGGER.debug( + "Tool %s starting (call_id=%s, invocation=%s/%s)", + name, + call_id or "", + self._call_count, + self._max_calls if self._max_calls else "unbounded", + ) + started = time.perf_counter() try: result = await asyncio.wait_for( self._invoke(runtime, payload, user_input), timeout=self._call_timeout, ) except asyncio.TimeoutError as err: - raise ToolExecutionError(f"Tool {name} timed out") from err + elapsed_ms = (time.perf_counter() - started) * 1000 + message = f"Tool {name} timed out" + self._record_tool_result( + call_id=call_id or name or "tool", + tool_name=name, + text=message, + elapsed_ms=elapsed_ms, + ) + raise ToolExecutionError(message) from err except (HomeAssistantError, ToolExecutionError) as err: - raise ToolExecutionError(str(err)) from err + elapsed_ms = (time.perf_counter() - started) * 1000 + message = str(err) or f"Tool {name} failed" + self._record_tool_result( + call_id=call_id or name or "tool", + tool_name=name, + text=message, + elapsed_ms=elapsed_ms, + ) + raise ToolExecutionError(message) from err except Exception as err: # pragma: no cover - defensive guard - raise ToolExecutionError(f"Tool {name} failed: {err}") from err + elapsed_ms = (time.perf_counter() - started) * 1000 + message = f"Tool {name} failed: {err}" + self._record_tool_result( + call_id=call_id or name or "tool", + tool_name=name, + text=message, + elapsed_ms=elapsed_ms, + ) + raise ToolExecutionError(message) from err + elapsed_ms = (time.perf_counter() - started) * 1000 text = self._stringify_result(result) - self._log_tool_event(f"[tool:result] {name}: {text}") + LOGGER.debug("Tool %s completed in %.1f ms -> %s", name, elapsed_ms, text) + self._record_tool_result( + call_id=call_id or name or "tool", + tool_name=name, + text=text, + elapsed_ms=elapsed_ms, + ) return text def _prepare_arguments( @@ -300,3 +347,30 @@ async def _invoke( raise ToolExecutionError(str(err)) from err except FunctionNotFound as err: raise ToolExecutionError(str(err)) from err + + def _record_tool_result( + self, + *, + call_id: str, + tool_name: str, + text: str, + elapsed_ms: float, + ) -> None: + if not self.chat_log: + return + + payload: dict[str, Any] = { + "content": text, + "elapsed_ms": round(elapsed_ms, 2), + } + try: + self.chat_log.async_add_assistant_content_without_tools( + ToolResultContent( + agent_id=self.agent_id, + tool_call_id=call_id, + tool_name=tool_name, + tool_result=payload, + ) + ) + except Exception as err: # pragma: no cover - defensive guard + LOGGER.debug("Unable to append tool result to chat log: %s", err) diff --git a/custom_components/extended_openai_conversation/tools_web_search.py b/custom_components/extended_openai_conversation/tools_web_search.py index 33f19d3b..d5f1d503 100644 --- a/custom_components/extended_openai_conversation/tools_web_search.py +++ b/custom_components/extended_openai_conversation/tools_web_search.py @@ -14,6 +14,7 @@ DEFAULT_ENABLE_WEB_SEARCH, DEFAULT_WEB_SEARCH_CONTEXT_SIZE, ) +from .model_capabilities import detect_model_capabilities LOGGER = logging.getLogger(__name__) @@ -72,13 +73,26 @@ def _build_user_location(hass: HomeAssistant) -> Dict[str, str] | None: return None +def _model_supports_hosted_search(model: str | None) -> bool: + if not model: + return False + caps = detect_model_capabilities(model) + return caps.is_reasoning + + def build_responses_web_search_tool( - hass: HomeAssistant, options: dict[str, Any] + hass: HomeAssistant, options: dict[str, Any], *, model: str | None ) -> dict[str, Any] | None: """Return Responses API tool payload for hosted web search if enabled.""" if not options.get(CONF_ENABLE_WEB_SEARCH, DEFAULT_ENABLE_WEB_SEARCH): return None + if not _model_supports_hosted_search(model): + LOGGER.debug( + "Skipping web search (unsupported) for model '%s'", + model or "", + ) + return None context_size = _normalize_context_size( options.get(CONF_WEB_SEARCH_CONTEXT_SIZE, DEFAULT_WEB_SEARCH_CONTEXT_SIZE) @@ -92,6 +106,11 @@ def build_responses_web_search_tool( if location := _build_user_location(hass): tool["user_location"] = location + LOGGER.debug( + "Web search tool enabled for model '%s' with context '%s'", + model or "", + tool["search_context_size"], + ) return tool @@ -105,8 +124,7 @@ def configure_chat_completion_web_search( if not options.get(CONF_ENABLE_WEB_SEARCH, DEFAULT_ENABLE_WEB_SEARCH): return - LOGGER.info( - "Hosted web search is enabled but chat model '%s' does not expose the " - "feature via Chat Completions; continuing without search.", + LOGGER.debug( + "Skipping web search (unsupported) for chat completion model '%s'", model or "", ) From fe54ce94f77ac8144673b935728be1e65331a468 Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 28 Oct 2025 19:26:14 -0500 Subject: [PATCH 3/9] chore(repo): ignore bytecode and drop committed __pycache__ --- .gitignore | 2 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 2124 -> 0 bytes .../__pycache__/config_flow.cpython-312.pyc | Bin 14841 -> 0 bytes .../__pycache__/const.cpython-312.pyc | Bin 4717 -> 0 bytes .../context_composer.cpython-312.pyc | Bin 3714 -> 0 bytes .../__pycache__/conversation.cpython-312.pyc | Bin 21507 -> 0 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 7249 -> 0 bytes .../__pycache__/memory_tools.cpython-312.pyc | Bin 22409 -> 0 bytes .../model_capabilities.cpython-312.pyc | Bin 1116 -> 0 bytes .../__pycache__/openai_support.cpython-312.pyc | Bin 1659 -> 0 bytes .../responses_adapter.cpython-312.pyc | Bin 3802 -> 0 bytes .../__pycache__/router.cpython-312.pyc | Bin 2544 -> 0 bytes .../__pycache__/services.cpython-312.pyc | Bin 6069 -> 0 bytes .../__pycache__/tools_builtin.cpython-312.pyc | Bin 36755 -> 0 bytes .../tools_mcp_bridge.cpython-312.pyc | Bin 3785 -> 0 bytes .../tools_orchestrator.cpython-312.pyc | Bin 16745 -> 0 bytes .../tools_web_search.cpython-312.pyc | Bin 4260 -> 0 bytes 17 files changed, 2 insertions(+) create mode 100644 .gitignore delete mode 100644 custom_components/extended_openai_conversation/__pycache__/__init__.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/config_flow.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/const.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/context_composer.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/conversation.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/exceptions.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/memory_tools.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/model_capabilities.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/openai_support.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/responses_adapter.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/router.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/services.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/tools_builtin.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/tools_mcp_bridge.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/tools_orchestrator.cpython-312.pyc delete mode 100644 custom_components/extended_openai_conversation/__pycache__/tools_web_search.cpython-312.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7a60b85e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/custom_components/extended_openai_conversation/__pycache__/__init__.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 7ff06c09daf95ac17cc70f8949f76a77922da47f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2124 zcmb_dO=ufO6rRzpWNGzd*|F8wjq8nT2X8S-XbW~iC{Y}j%88|5J2@znWvgA;tL~4O zT{V^Kg5#71L%=PVq=z2TOG62T9D8h_xAtJw2DuA?Kx=4j2KUhVQ2J(9s%%v~6gsdo zZ{Ey%Z{~g9y!}2J&mR=*8{nLEBmz$J(vr+azG2|T#f^|Lk?2Q{5)nF=#?ppr^97UeQ}5e~AuBSUEh$DS%}<)fdkV26TQv=SbSgV3B%f2q z0(QcZVVJhNV!@P285ODpi-Ni^W!QvHqi>nIlCmt-vL(X?MewR7*$_#8jy*SlOWB!J zI+L$w#XU7-4BbWu?_UAoGQyChWmI&Rj&?=n=$y`MYV9@^J!<3{Rq)r$O)U79;u%R- ztdf*hcuTR%rR2Q8IIJw$lEZ;QRti#Cvq=a%2tJ2hlq|~$LF!QqP-cLf#}5mYR7xEs84SR}_<_3S`Zy7e%1hVzoOq)&@(o8 zCw(hjV~tKr-rp53-b<5a}G_(z@ZX?-6M5<5~%jx$}0bbsei0rB> z9!8cEx_Tuw|7Lb}W=`;tD0S8iU7ZW06J)3B5U8Fpc>(kZy6*;b{|>Ei=JCkp=)}q; ziu&C`mF@jRxo>=GRN0XoE>`0Ps@y=0o2YUV-}HVL+Tx~b?DQr(4V2hpkpD71u;^h{ zQ>RjA@RL!TO0rLc0LblWat_|yf29dP^!I~01Q9+A(T6^OybT$O^cA%<+bxV#*?gWw zn%0&E^oq5pU@GIr88D?;o0v`il%&>R`(5oKHZ32~+5`<^_~sChkJO7t*EK^kCD|pP zN4Ja+Bt5XzNq8J6yBow(y~E74y|2-Sp8OI)ZXE_~bjLue~8TZ_-Gq<6fca%bk&%*LtD zhPK#&8hf(Jo_sL!F!@FD@$fb~4uo1iyLNWt`j6ogJHg0$Xf3oJU5joMwu1vZ;hs&l zXD@`h=74XsUabyad=Y<%FZQtyQ?G%T5dz*lC?15uwiJ`qynVy6$rukmhPRynfnrMx zhkeI1wIkapfr+1X^g?71I-%R?5JlO{i=xAcVxbJCiYOAA7AIg=N~*CyC@Y;4i|~Je z67Bt8l1HDl>(%LQ-9i*X&*>a%T`=Id9dyMHen6(|bt045%=M|+xzv0*oAJ=3DR3XS z3&U4vALhu#>L7StH|4UXyg^<89i@>q4&5GuG5!g?x{HQ(QDPUp{uGTpMWef@?^(3B f79FZahc+WGuY~pk436(d{5bwwJb-(N0JeVte4EU$ diff --git a/custom_components/extended_openai_conversation/__pycache__/config_flow.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/config_flow.cpython-312.pyc deleted file mode 100644 index 2ded3fc8e93c0ee5ad11b9fd06bdf5f6b4c4b1ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14841 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 diff --git a/custom_components/extended_openai_conversation/__pycache__/context_composer.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/context_composer.cpython-312.pyc deleted file mode 100644 index f5185f2ff8680160e615810aa5f2de617e9630c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3714 zcmaJ^U2GKB6~6O7`{UiUu~~ynGzpIJ64xOrBBUXJZ5;j@gK*LZCup?n%&y0to!Q)( z1-zM+tU?t#ssvGJ?TA#}MptgkovHxqyF$v_1rrd=Bdt`!n6QwyeUqL@D)s{C{N4KCr(Fno<%2-CS6CGe3uR8z1v!6I@;=uKyUOr zy)iAJ#jf+yaV@FE;oA{G-~Xqjv<|RI%=4Mf%GfF0Ea})$TwBT6#ggsl@U>k1s$0&Q ziZk!HdQmB1=yDz9is8-}7TC_|mU5}A6?E4bc}n(}Vf6QM`@I;#8kQT+xhi4n{ImgAmU}i7rrq7e(`kZ08zDUpJ z`1~c?Hhsy(MzN@Clb;K|tg2SFsH>_URn?-cl})0@RrP%Us?{T@s%Gb)IjO2!&`{OS z(FQr?v$0&+aqXf?p|*6(bzX+0bxYGV)h_8))_^YSGF%A7@#WT41on=OM@sWphKZ<1 zfa2@|@iX*ie(*^waqUba+Hvi4Bc8l=w$agf{VkjhZT~mogV6nd5a$71fubvBB)_YL z?l^T5gWPjv{{`JGV@tW5HOsn^2R8{ZR|D5k9X&@qVgy6zAYY;J>abfi2ggSm|^um;Se&T}q_PMhY5N#Ot0|x}5U;>n%4A)l!_r?RH z4}Q*m5DVyQ5yexDWNJN@SbYE1z;fo}%xB{@{`Tp$p<@sGkF9ncUyY5{`B4gPuEk9- z5=aq#6xT6W4sc~OW)8r2SU@qxF}%5|Y7E228 z0z%5fVSs)w8$c1?BHqA7eDr2T7<$inDf4zl!h{j%-TExneZeW222fhI1k9rO{H$$Q z&qzpD)1B)>&2{L8CJm;uGm;}eCblzFf|>>GdEMV=KLr% z1cq{4KjPxDl>ls9`rOq6RDDgJ3rVKj(KK7En_}a^hy^Pe1r{ppopn z`N}PA<=`i?^`uglmA|G3Ywl`#xXyPsr1Z+6N7D0+H2tdK4Ra9Pez% z$s38K#Le=Wyr&`euAF%&4?dK8n=u&EOrXvk%hJcvinN+i{+Jqkm>OJ74K18*#JX?U zHDh(>fqMh>*r7UqCt3Y1ZyiuT5VTO=v9f$6W0J4KH z@pVvs!7igV3wz8}#-Ul0$A%ky55TMPRiP?+JWemkvmv-!j!9s!1_V2a2Vu5n%)1r#Vl=~JlJ3#?z$B1y5=4fho{BT1fe=4L zP}^)68hma}pZDXfj6o&w+aB)6g5-+6n!X(3wlzu?7J?{=K*$vgNV{-le=tb@&RcrK zv~yY0IXbdwI|(~*i1^n6S|57-{)yk7c`){Sadqh2L;1zU_|5!E;c+b8htxUKfD%yZM|<_qbt2)-aGW*)o(bqKe`yZIsQoQrT$z=oX2+FFm>gG zQ2?UwB_JeHuXIi6jstmsGL^$wH#bwtYJSw&)GH}S53r$M29J45FWPwCkKnMS!bYQ$ z85Z}!rg1-rOf*20Fb75dSrj}@@svs1(lLP;$B8&h1TpeOm<+NHpC=Y@L*y~@Mc5gD zM^gou@(p5rl!#-bHxc-x+B$*X*{ZiU?WbBzn>)qCPlA1t6cxV?!XcL*(%bRl!PTX| z=f3bsFg{L36+rwOSZ*g89$7fs*xkQy>L0u)M88fUAyH2}SCj5McYoI!KmM5Ss3%^i zov9xfeQ;!rKTqpQEmQx&8}~n0#;qx!o9h* z*jSw(qgMycOkOHX7RRGR5C}73&`ObR155fVJ#M*V>Oo=#$;q%n65)%PL;&P*G6hAG zY6Q|??h{0#*E6xHy}7B;l}S#PN#bt!kQ6#!CRC!Tm;|&R+x%}qcQBy~tPrt}^oj0# z377?w902bnf&^fY3l0#Cydf0hZRz4O`ACqcAH`?Egpi{{-r*+4FwA3g>MQiZSLpdK zQS3_;`4j4cx2Lbaj?(aDWB+7DrmKlSY;aI*(rOp&;F*CY0@3OqT0&sPn2l5q^J?=z zA2ZH0I?_#!C{I$mn*vc_QcxwTMo|yEEL4r7-o9oBRTC(^yP2eF3W>enbP|=`;(q~) C*^DCq diff --git a/custom_components/extended_openai_conversation/__pycache__/conversation.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/conversation.cpython-312.pyc deleted file mode 100644 index 8b6cad01111037dc800506e39d92674cf139d4fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21507 zcmd6PX>=RMnP4~2jr$^Q@BqO>B0-T9B~lk>Px^1nnN?g?W?goB6T3(BXBW zGRdrQ;M}6Jzo6QCaLg0r~EO&;Y{s9wm&`1D%?^`rQ zU8guIL~%5yiqi>INJS&8j;ljz^2>x6^2>%;_+{dngf^s2=t8=LKBOmg*|;HL3>g!q zkSSpfnG=?fh2(4E)`Tr&Bd|7Jk+6sC1lGkJ31`SjQ+n)4A9p3(AvctmIZM1UQ5C96 zREMe)HKCe>Kjcr;hH4XmP#{qks!P;|>Jts2hD2kiF|i}GBe65IGtm@kN;HR>6Twh0 z(GqG&>xt!|*A zo<@pqEsoRx^W+WrXe#O(Yn57;$07H)tJuPfR6$=x=*#qms@RkJDQcX#LS0oqMO~qv zqF%Zo!@;VV1HBXD&qeuUBo&(&4@|}*sf!bQA^=gK|7t2a&PBPv@MLtn=VYL~X8S)O z?jS81Bje)}DbhMQ6jY1qp7Ci>dnT5O@{zcx8=geGk$8|1Z9U0kESZXorvMRPPKnyy z(Mamd#3j*Qj=Fz56-!NvzK8P$qLI`TABA2uF@gagQ}6K5K)C0zli}0-PeEFLVPYbF za(r?s1&IxK&qb3{am-~Sm!S8N@+mCN6n!rMx!0*E6{2Bnt2i3gGRvvpsW6;|Q^PMi z%>=c)1**+FayB*|OGM&<iLfMTgXoLY z2d1HWjN05w-#4&ki6~O#pK$QO3Q^NHe75K0kZ2}5?Rd}m{_x}H&WJW;<2})T?)=H& zA+dtw44)h989Ev2xqw+#lGS^n=R)}Ga9{r!(Lpi@96o>HT+fC6!KXww$$T6TpX)#W z*znN#{`0t_i(OpkKl@le)O`G0zi1#eFAP5xeyoW5Y|oS73&W@Tht7*GQh2Vv=lt-{ z$)Uk;|G>cTxeKDDuYaKD@iP~~kDVJn``881E~m?y`sBQaiEwNrHr(_0g<;XRDHA&C z?GNKPMgNvX%D~|K@o~{qbR>n3Fd7e!L?$E8#^SM5ESe0e#0GiehEuTL!WVhigK}09 zAvnXtMkwMVfa2oR_y``bfF&MJZmbQ+D)2B&4)Y_UQCLWk)C4cuF{}S-bYu$K_4E7$ zKY~Y)9FW6*0gs}+0Gy_1*kLLvtxA;+ASzAEoaGWNb2=K1qQ6jIp?NzrgE1=1L*gCf zTShwHfmJZ0kz`V2;cVfjMSCPUJw6hSa9kKN$ZRB$E;x&%{Z}DXNWxZ`#47wW-v{L< z;g6gk|49A(1?mIFz3M%<>|CD8dHYv913zE}e?iW)&$~SI2vk)Qrnl*={|z%Pj2zU?e9iur#N4mU&4pxGHCve`_Ge@CHr>H6>1l zOzAo`O#=@S=@z~Tl6*Bp<^0EyoSK}R;8Rg99FL4&ngSsvIn(utWNp#vNyN-T1YF80 z9rzo_!mSS5Ly<2ZX(cM=fe4=e?vVDjwBcL4x8BwRK8AKiDTGM zp94%uZ`y=F7Y-quHh=%1a!p*&5(HgxZl%Ts_|3bK46 zj|BQ;Gv-u_Tt3z&L#1C%lQyJ{X;a$#2A+)&U`+HFYGj)N%mUaTV6Am*Du5)EX=o~i z!V?J@OKP7CLkm2NHia|`3ZCVb_Ko5`$XL_Xu|u0GK~i4jsNtg$xFs%4TAS9T^|y5L z7&5l4XN}X#b5o?aTkdh}hzyl}<-Ri&Fe67x`7)HTr_KDN9_IwP>ZXvk!OT{q?J!a! zux2Y6ui=g2uKWVUsXq(s=1c19>VL(uRK}6|l#C$lkg3eLQs$Elzus5+9~fmnDaroB!x0M2~dv01%$1ig4}vU z^%Sg%jPrTt`Qq;tIt8{!DLh2Wuv}95q={t~S=dTAPAu(g#mWyk;xQ#(P-k4aHC0+z;b988TmV*CTi6D(lb z$Yd;hISR(AXCu&fijRXi6S7eM0OeRTP4Jf@=d zWLPQ`^*{wrRCoRf7?8*yrJ{l4#?UH>NK5KCj|UTvC(leJn8t|3E*VIHiXONaiN(R# z*&W!Cobi>)z%Ei=gmyeM6!7N|5ef~^B^dl>TxeT;7*jRL?uCKGqi6s~Za}tMv=nW7 zMYVy)!&Q=g87`8Fh_)~ny%+%-1Q-EHduf>JOVJdMq8^VtmfwX@D@Mp@dE_lTGEyG- z8h-#H(G){#MkF~g9vi>JAH*!?Sr|9J8>=9n6t&?q!-IqU=R}r^K09@ZOp~aWv`Ew{ zf>;x|717*87K|f6MHe<@VPV8TtVP%n=BLI>DOdCMIe@R>tl{CLHB?Q4NjA zJc|1Mt0Pe|KIl&d`NP=87EX>%Ou-7pSw^#}SdHr{d@%-AZdkTO6L+|{#zl2_;=HcFFjZG+NTbR@^A!Pdai%=nnIs{l^ zwhEGU##P+0qhycEqo3LTi5kV0?Ji-Tiy#ybqt1s0N`IiJ@xA;9FaE(SBiMWAF63P; zSyxNW)w*KuU1Gjr_?jW#aXi~`9Fnuzb-Vj|^R?!ikL2vlvs%I7xqk54!JB7uj+R-S z;I6zrdTn&Ic6ZL*aoc@xsc+UOc&e^ny>@jquqWr)d)w2!#Lb%4ZJz7KYsQA_W3O35=&pUQz9XoG3 zjxRR;W&7*x0Q~j9tZvQI40T^V`Qk~zStYn?;QxI+>ov@p3MNYL%Ihk#y2@2u^-n4q zHrDet2)-SF-ENrG0Dh1E`sHhv7n*bKUANswZatrO^<`asIoH6fL2&x74__Oe)&Iok zEvW3~!$PnvAMD8ndxWYwp|Vz}4GR7yp&Ag@AY9ib)a-!&1sBzFoW4gHTkIcE#zy;! zEpUG)Kt!>Pr$%sB2~|6U$_Bw( zi|7EZ2@1YC_z%QYwgGLG7NE^(ne7A8Y=H$PXWaphUuVkef>~W~v3cplGPkPhU9;MS z3YXxj&AXbju4bWnXTExWwt7Eg0zEbZ&;yU@G7ZE0#eFa3+=U-MMm>|Id5a$;dDzw=Ob=b@aZ`vZ--S~r_vIY>e_-1G z%QdT4uAVp5WKA_Iek9Lye7gqFoBqJBl79_5_Va^n!-wf_clb|xsCO;&X&3XZl|k6i zQv(Hmx5t0Bg?i6QpKWH|voQ!eduS+l-$g^M_dWWvJJjz7y3SOpzvH9<{*H@<((hE7 z5Z>8!HlY4)CFXy(3iH1kU=iM-M!1;}+y;NeuVzL7$~iG<%0w}OXLE4bbNOt=+19HZbyQz8k!CEW@hj+JqPJ><7q zHJm0@vM3dwVN3^BRqfCWducM&w(ruHZ(#V~7_xf^g<~=Bc%Vd0BFe?45L5n0p z9tT*GeZ~+Xq7acNQJ5^rj69lT_&pe*35M^)s0$;sW0jfH`52^U_I$E+f3K*Z*cR=Rdt>a?dlb8g3Z! z-j=MlCGTy|dfRW!EO+L-$JVwc`pxsXnv-j$x`nQ@&+urJ?mDBEyowR?Ew zQ%~g@LLhO>ao5MLjpg0VS$A{Z-I{f`EUqo2ra4t>#%UZW%evBhaku(vcm5qj+;jDW>C12a{qx zmh&@U%H*_{`(PokVB#}!I>evWjBRnq^3`B6+p4#Rj=_wX)2Gcb8t@qiKHXM^S=dI( zu~r&ET6N1PZ?%jiZ2`J0fX9S*z`#e3DFMNRY~_#UN1y`mfj=^`3bi<97dLzEYS85}3@K3vjiMgU_;pO$VvjG`Jmg?Msi zkON^KR=SK49u4A#3rZjXItWAeC2dhsErFP#egwKd5SBu%uc+!MMWnfO5g)Wk3Y;ml*ujZ z?3f*#8_Jp*e(LhhdtR-&QT19l=i7JNcY5{E>745fs0c>aJpG49g zilg@^RfS=ey`!N(6{>B7%7a-`^}45h?zz0DDeGyG}Nej@~Nz zTYKqV5A&_gT7ciO)4e|CEeDIRkA_$gBXLjR+42QE`Ig``kxo92&gk^z=bBnMq4a5e zQk4crq)SxB0K1dfO4PjkX;=h*Q9Adw5(wdB-KKS!K^#wI64J^=Kp*wYX^Q$ZY|M-i z&&x5RT&MI4VhyYj3?RDtL3G`^7E@Ns2!f(=&6YOGC2$2~7_&;a%43dAo59sidsFF& zrd~SBYZ7+gj$W6A|! zX-L3ltZ)@$2L4kVk1`kX6%1TgZ1-%W>!P#;eo@J=q-~p?)l2ZK4AM&8hKv1kJ~JI> zoq^Sr(aq5>-2z_qGx&>uNnQ~>;j+M~~BW<8L9!tbh00n^q zf;$$NC`A?gIX5_I71`ZP8~R}dqOv1aU^Eg3*A6goN3O;aQ;8y4;5A?@=GwtUf1_%p=x0z|~QWHV$bHE%|+&GX2E$vaQXyiAn)wRIy>%b!IAX7fvVrNxGz_?=PsqIa9=w)*E{bN{PnMn z-WXk&&iVJ;_75*z_{Ni8dvclktKWmf+sSu3-?}_?S`Jk)a~7m zE=>#d%`jq@8%7M0O&ySSr-K4RDR}}_b@{4Y*{WUns=e8&y9In=J3Hw?C-Y8|3E?gp3f|eTAFN>CIi?#lvESCRkpFEH3pwAe zU;tJ`6jW!B0TU7BHSl?mL=+G|Pz`f}KSq(&Lmee>G_X|FO_j{Xf#GajPYvqb#(FAH zZ$Q<{m{K4PNdXkCqOXOVgTJYV)HjLgq!eXM?j2a2nPZ7s=K^I;QP4`*!1F^hPIFpd z!{CXMw%|zyH5JQMS%l?wMpv1{KU*yWxT02xoz6ZRme&|Z%cc}@UnAL zke}`Nsj`{za`w{9Y{++TPMP{rehGpeou!uGovaAMr8*^OQ*Afrk#Te0n<~zCQ+-YK z(q#rlmhp0xH`N*6*1JFLlh-lKD9pz*aWW63C%s`jnab@b$W)b;RFzYosoqdhMro!d zT@B)IH;BKwu|1n67n14fbPbq3DsNTE&+Q@c*T40sIDfh_?dNJYs3zke?t<4C7a+UU z&eaid)DGh4+XpC!Ul{>08RzQ?np1XzXo_|S5{PJ8%T=eVVZGI)Yl&#;C!%TX^R+Ma zf@liACDD|Kr42)(2K)_D(F8vWSonVlk!)zu6)&BO))f~K*p#b&*_tp9Ekq+A0J{x& zp9&mXCnmvx0WAz7B^pZ7SW!wPLSH}&0X z1O<)a|0_ld7<~n!e-9BDtw7)`))Gz1cu{U9ZN7#zn=xvFC|Jq=M}+?vqcM!|5%P*D zNpwU;&|M%2HZf&Vi6vTO;W|H(A_i%(3g$_1h9}~!WNX4}e-KPDHliPV(J`YaGLMW- z#73e?QCobr5`kYu0$#&N5yJUJNQzo`rvgQ6{=Z@2>lht{C>Yo*K7R&_A{ae^(RKzY z9;ZUoTn0~xOG%zA9W{E)&?%9YRt*22u!f;TIsX(s@Iv8()l#(Gxec^BW| zAQ}yW4_VX`tqI+vMGFBH#f2}4=mEg;u!z1fM>2+`q2|o)PZrqGI|OH+IEIX)i0%gg zMH11gsg~AR-3LY&SikZ5bm7V6mTYzZs%b#b*ZVuIi4h@9J0{&DIUh zYCh2rhOdGhz1?;+YY)D8X0}(TY@8jK>wNJvF^e_aHZ~|`u{}B0-WA*4IeNu<31h;BSC# z4!k(LZmXDO1&dv%Ju?5?tDn8`SxLCh*B;5@|G_sk5aoS?v!?`iK(h8PR%G3~^6t*8 zyA$*hcg^*QYZJHKyB6)UgKNIPf@iUHnaTN%-1hapX`emy6Gx5U@y}V-Hz+WI@5G!& z(PDZZ(qirysJ(~go92BB^$SldoLKR+t=RgOFG93YnYi+{w=3YK|6lm1Dn3j=<*ne4p{OjegrqpK4~_wjB3C;+>|R_EWob@49;Ikovo3 z@2S12_bB>Qhvq$+L6~iX{P%XT7{Rq&2cg@@c@4(PnfReOd>CLe)06 zJxb5$P@UFF;%HjO(YI8x;2>iJ3*}J|16ZU5Y+hO*Z&OGi?Igh6iyG)wgHi%5ZmDGf ztAuNs3Nk|%z_CwH7!x@UC6JuK$m-zL8h96%$M>q$l2?~<+y+_E86JmMxFhk{Ot}?> zurM%{QDRVXuEbaEN~dx0&L)iSE`!NL;!ivdC1{X%hjK|#cD5S{iR6AKNe+0)w(?!< zhCyhPi$UY&!WvJfvR+Bri~RER@ZAV@5F9*PDXi#^v5H4S7Wv9aap;`+rM0!AVr;@T&A%2}Luj|h0y5a5>T!cPoXt|+XVuY%lV1hAQz=R-Q zeu8~R3Al*JlGQ;jelH=TfePvoR)Gx{L8P$doz%MVU z3|m&ha?q<%qTWzaW+Klp<2O^KrB9dX;>B_CFToO5ma{Ta_#%V6gkh^xfZV@zEmp3T zQEgL0u`Pgh%<`RO)UaDn(*WCdD{2(uIT-f0uAvytVa)a0&{B?N>+zIf!L1F4kFhi- zGL%+oUBoxdHA9lXin3}+j3rwqsNvtnTomx&rZtIorjhZHsL0^!;6(ZqU14}z3;dPI zhEQZAXo*)BWxJ4Ae|3E1N|+oR-+@x{x|XyZrLCBQ%$a?kycM@NQGS8DumJ5}pnw_I z?*unXe?7Rzd8=o2KQ-Fty5^m$M*nL~i;YW$)%wG0?wWZH7+zz}y&I$kN9BCiLgnIx zoMSIK`(etx=*&6VAw}5a)4&)#2VOj}X7f|9{~V|F_;-Cq^Xp094LxE ze9z(h#yZ%Z1f@G1jRLk?41Uwp3#u6O4`~Y1ppYu& zwyg|kprmTSumP{u#b|sj<2)tblmyj7q8e70$UYZ|Peoz<2`~o!1B}Q805b;O-6D(E z*-6oQ3f{`^i;jSfM_xJy1@1>s6TIqiEpthnCfU?30{rW^4sW#$Y=9{FJRkrE9iwY* z{&$WZjq_EJ1(PieFhBsBKrrrR` z(nm7tu#!dcs>jvLe+DB0MHGmr&h?n&Fgk(}5t9fqGX2;hs4K(s@ilE_H9v#s{@;3( z215Ea5Q-jHdO^iKGjU0t357UGn)?~{g{G1jJ(+g=3?$d>07EIdvfOb(f{|A7>gsKP}l4W2xc0M_*J4Vc@A z(GHAoc>y~_g9o_dB&9D$Op~vqz&BDzHj2+Y3Zd||q2Zwm=Z4RSUa6$?X8c9zyFy9w zs=ug%S9!oRmYU{qWAbM)BDSXU2z?Ht7)CrsU&0724aEGyBRdsMz_L@Zkwg?EX^zLk zjK}>Xz1ClekQbvz*ZJ(#b3G+X=V3R8WLQL$C< z0)fVIS4Fdqduo6R2m$Yv@*0%91|<(J!sLp_>H8X*4Hh)d(X2tJtSP7o5bThEb$!)6 z7IW@Rs!gn+P`wu-9c6PA^avR!m$zU<$V53k1v5ex${r|K5pq(V=7I|$H|3}-co6bZ z4);ADLZkF)n)MWs5zvJ7%>=Yzg9-xLv4w+xA=SlF69P&E5Ky9kfD#D=lxQHJL<9km zih_@Tm6XAJuZn<=sq0w7I#AETFH9EH(ITXy?9~N5LeQPRU_{7787%kA2o2CKwwD%Q z_-X{!^^e?R5h%1%8v9+9nf2pj+Y1O4{92f0@6Li6fA{W_evdp#e%JNp`z+)WL4vG< z5m*8w;ND>2k1&Q50OiueRvtlV`j#SiXBhDKz}A~W8ohPUTSF18!6!$NE;2UrLvk-mt;@F>`dmXpM{Pj-qBZIUtxZ(fCylhNqNC^?#vnGu8;+CZPh(Od9y2604CUGr*$+WiD~I{zh}DN~^Y-OO%_yP`~9&f<~|6 z)VCN}&4QU^hxq>mJvtmldDz<~78ss9a?@at)bU2f3wZbeDsYn_eFm|x1tYxZEPHpe9g}!XxxH)sJ}ljj z(E*GOVsr>1(JtNSlz-uhybCH;l$>TGTx1eVS>)wP$ciw_25GU zV$3CI(tsT$FzUnTQH;oGLUsySinvUrQ`QU61KlG)5qvi7D9^&W$EBbAAw(du)AR?_ zk)KiZ|3F#(o-+PDW%)4`%u>OR7?xInFA1&s&_vO}A5pD8qIMTFF53PhX6U0Ck-& zfqOF+Ev>odgy_DLrfYv*7_I<+p1YKT2Bu6IDHQibGudDpIV07N4tgQVb z#;mvBqaZ4Z)0~s}|^H47i9wpg@6k(;WfHXuw6$d+&Vs5hXNs z;{kZzoBKX<-+kwP-s4|dTSFW?;_v=5JF}hR{(&#!6nRBoD6dDWNZGrnmb z&-u7Boa(>Jse1sfH^Ow*|PN8g78N+khLc;RcD@3f#6D zu0-4jaNBFREyUdp+#NOC5OH?`x1)x;ZT`7fXJL49R+F{cNo_8tXAM)%o0=+LpO?pP z=QKlw`o%fT7&|Awnl)}|%v7*f{9m}_7+>0PHk;ALOj9>=ijf;tYAL|9T%H*dNned@ z0jT57t&ITrE&mCZ^BO<$EpQ(L0v2G;7x>RO)py_jl{1#-7Wf4&;mtJfj|r9S=L|ik zD`~v|szfEkD?vx1LZ~X##InC6n)x}6m83*MhcSr+Lpgvim|8kTFQt}5BAHf9Gm$V+ zKv}+18A;~NTsD(PW;1hH1Ew|)YtH^8vammj4o%hlfkt;f<8$+@6=o@Pjn}C#?g}75 zqxn3L@9%K`lv+OuK9sr_JC?eNJ;iM+$F2U8kEGGnp6BlgAGfVRFD}U=bskg$DN%+B zmuF8SW#yD^c)PmH z+gjpT1ZFi+0GWZsa-|^XD8~|sLg$9n5g%7>b^y7<{e641D16FY zhGhx&!ca0=B2fw@5}B-;Ph<NW7O<(oPF}NX;gJ?T2|up{xr9Xm$1~sio721PI!W zUqlH{k%SB#2@$NAF97-eF87@23gQB?iF&5>JPdv7`^WPj4BpZ$UQS7PR zQ`h+IxhzqJ((FEQ+65MbHT-C%#9B~a=$8v&G92gI6Koqcv;lFA!qzWgyQgtE z9=e~y0h^(F#OgowNE&m{O)9TLHzJ$RjT3BzZgdcUZ$Cq*+0sX3-K<>zLrGXCkQ)3F zh6>$Hq$l+DVs9LR=mn$ve9xzQtnU5ANDY2jH_qGd&g-CV0GUV-;#Jfg`kzpTOJrP!d!nr#L`#LktRtxSfg+`yg-~-R@ zU+@DJ?guyX?+XwM5;!M43x0U>zBn(&0tDDfSKf16D75UZpu)#1+vNB4+^mcu%as#x z`J$o8*_0gB$R9&|iXN1s3eyz3nq_iS)lfC~jwoWO@RrVGzT1Kl`v2MY10V zG@)-~^<3K)L%!_|3M}u02qMS39~PZnSK)Ec79?QCFd|wPis>!6?)4 z(AIG`ysfA<+uI2^6K!ie08MNN$w4HCfPkSk=&89~HqjB;B~D}>0fKh9zu570-*R8E zb>+>K+#eQxzp!FFc*D9h`R%2*9$tFu&mVw#-sZ2OmfEL~woLg*AF&qJg94$Evpyud zkn9Fh3S_U}(2}`ooMC>wf zTago%bb`ce9*SPX44$*fZjoB>cSR{{_mn2}n6_ zX&xl|Dg7~sDSKe3IZgL6$h zytFdBvd=nt*6Kg^NP2x!_x*$zJb{ur7$m8iVz6_A%!I(xC^8N}r~CZU@R!HFIJPuo z9r~#iIcZ5Jw?H7CJsFadIE9yj$1oY{Sp@$G#nL1qum=%1Y9nyeo)O2@ArRM&&)QFb zY)FPgN`Cy1{0MNo72zl=)E#95r5&z`ozE)jX+=*%^4&}5^I(^T7CA*tae`c3R*EKo zLabp7Ye;h7{N)~8s|n4#51@j1b!FV16wg=oHypof#^tLT(^K;v^i3Hu68RP!X;oP_ zWJR7=GHEzkvy1}%&{y?`P$yT|4)dEBsO!>1q{@?*>;Nd0vS zhpzU*Q3IcF^E`Z;67Ey*zPw3t-P=?GLev!)W@2&#h>BdL@4<|8yopIogEJmBC_+=> z-aIHH$QY70;Ai##p`g6?u^@_}avKC*-U+jGl1NiaPb59 zRc>VgnmM?;(oHk3#q0g!IH=F{k67Q#Kkja(A`hXzfRSblNCQIKV#Qdo&l*;&@O3xu z?H$A$l8vB6OC@XlVd44=3Vjv`glk$rQ&+ib6Y&Y&8jLpak^&v(!Q$lC?<~J#9UQSn zbSrYhl5W`OYkGWq4Zn_L8t1@6hMQOh>`MzM@X`W`a=TBw=^*;1gJ>N2)QAo^*MQZ7 zW3bdciQ9i(Gv3Y3j>8p~2KSTp0rD`R+P(^d0~22N$Q&8w76gmUAH1h zOG?_Fz3FkQR%8H)kt^QbR|hQ4Rj);q?~dk3N~b3Bn^87 zN8sHhU0GG65&K&HB>Oq=c=(yW22%F(JpUbc{BPWm^+1R}W<3{O=b&8g#h0#_^Ah2w zeDsEu^}Qi}#AzGBj-$?~(e>UiKgC-;L+cz?*7vmXH+ie?&^m{eb-9&Sd8_|Ld{yc7 O8gF&(e+;jVI{ph5T1DLe diff --git a/custom_components/extended_openai_conversation/__pycache__/memory_tools.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/memory_tools.cpython-312.pyc deleted file mode 100644 index 3ff1ebc79971b982193fab66d851778fb12d6bdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22409 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 diff --git a/custom_components/extended_openai_conversation/__pycache__/openai_support.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/openai_support.cpython-312.pyc deleted file mode 100644 index 24dfe159a8e7a76c2614d54f04bf8c628fd83886..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1659 zcmaJ>O>7%Q6rS;}*Sr47e{oQ(W*QfYCF(Yz5>i`1U4B#))JQ}Tsu5bPz1w)nddHnv zC${BE0rda^i5ub2L$4*=;mQReafw0?+m$%c9Jm>h1IhvMX4Ywv3$xm{-@Nx`=FKsI?5fiflImG(#^K(*`&g|ew{3}9KZ4&se7*D2b^rULF33bU+$3Qw-;X}ZSMN3 zo_J*+5}hyA-sPbz!vHwj=Q*zJ~^2)Oy3@U?*rL=ypB4U3yKwi zmr+T*{cg{(0VZT9kYy6U@uyY7^mZk(2%JGM!IPs@dnzwR(D$+$H1@ph|4Sx|0uqjA%tbd8&yyXKg( z7M6p#$Kd59Fq`OUW+WV)3iZs_#uvtynaxU=&3`+;lP&CKDm!!6e!RCccVkDb>_ajt zDUn{YxMPYkGgiwr#oEAql%;Id^4*@S<2j!~m>Rs2&(8pCq2M`>eY8Eq_mhFxuxy`_ zzQQvBNIY)+#8DQT3}Bw27xxo=1^AZo?kB%bY!5HNG73_svQ&sF$I+77`|blLXfvN! zek{#c1%$G=dqNpQ;gOnEUjs4Ly0JD-i87rg&{qCexx`whuFXl!qxCf*@#W?B$y*iT zQ474T&t2OgLBk==Ivg^%X{#)!?WGqkFI>4efBE9Vd`XQ`g7P(|8;!`LG(ib1>iPi_ z=NV;0T}QH4*(CVbs4(NgoB>l(nIM)4hNKRdIGvIfsSS(s6C=cY$HTbD61yaXWdTML zHF2>|-7}VlfOz?bU^dagG@2|vy7Jq^E1QeY^ywY_{Nrm+t}gy^>&exXXQQ*>)J!;2 z45y2`)2}`1!}q{Qj->}EX`~NEP&OZq<-_q8!!zf?saXhRH3%g&HUe|QCd9AsFn4tA zKjH!VIMSFr)kv*T>Xmf4nMi?9%)vAaku))hBqaisa#6}OZCW?YC~cZ|+aL|cG?|cE zqy^m;92XPXisl-$=>SbG4=g_@Hyp18;x9`v3|W`#Mp}FgCAX65$atqycjQkhW?k^~ z7|Gp8LJ#%pO=`D2=M7ebkhm879+-iGF@B0x_R!oOn%P5d?xC}Tgn|pds~7guD!z#a zFQD0Cc}AC diff --git a/custom_components/extended_openai_conversation/__pycache__/responses_adapter.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/responses_adapter.cpython-312.pyc deleted file mode 100644 index 7978ee4119f58aad6336eaa4c9173805c58fd770..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3802 zcma)9U2GFq7QW+|@pxi8juR7-V3KL#{J0HHfpiI!zkrjHLLpmV6@pmr#4`bdV_SE| zB}Nl9(pK6~A=Ri_#nmEhkdT@ceMqaWHtY**+vTMXTQWvCqdZjF?ZdtW$V!NP*>lG~ ziHo=NMmf2^-#Pc3JLfz3*6FN5(7xD#BENJX^iMJ5g#O_n0n17v!qQ?YN3aaMyX7G7NOgPE8<#>x=65o)NU8X*rHJ<%h74cQbMHKHDQ*gB^hN$hieM-)w+q%!F&d*#*E%!?@Zx~SZop$D z_60n)A|6}9vuaj}y3~abp!&`>mCuyoR53Dr8Y@{F z!SpikpJh;rPw{O?HB$k(nGa!_LTAvmZ!e=s`V6{ERa#B)#Z`Zm=!3^wB|v*uR+Bg; zjLC|+O&%YQ#eM0L7C9MUxoql0Zj7G=A@FkMb#v@o!1x3Y?*m)rm6GUMmF3Hi5l$=O(`FqrO zNQ#8jER*kdrn1dxy?4XyxURINiB$j=+xc^Q(TijAs> z5{W6Q7z@h=4T=nw7?dz**kJGjlVLeQa;ISngXQ8_6p)Ek1`~}as^4x{^9dJ|$YYWK z4JM+>6WC^U6Czd(Hep&stB}F%=CmZLqCtb<3K^4_7uxf~;iRI*Cqm))1SEBEuhIke zkYf;@P#p5T7=fi2iHMofdq8_W`G-VFOsFyrBvM9Oan=lRh>%67IHiytFa&M^&BRFM zukh}s(V7Q2>e9c+?3?%L_MYkB3g=GyZqQlIJLmbd>1NX{Z_ZUWV_&nP>P_iiWk%*y z-Pt`oxXd|MEe_tDYiNFgcxPvtS!sG@&Nt65^y*E0OHC&hgLjYIIdboezV}3$Ti)E9 z^LTUKwkL>nccz0Y-nO~me8+-M_wHKq9$oCc+jpn$-k|>G(eyxWvvB!BI`H0@$ zx75CW@z4`wY3$7eb1iLuocv^R-m16sEbP-;4t_HVDjyAxJQyDN?o3D@{!MmFemF3m z>wIl~@TQg(c0Oj|;3^HMkE@ZR`q^41s%dweshp$cd%JU$gW2cG zFvRe0sK0iw1$|ZbI>@h?8X`BbL~fx$e#7APt)*Fe#g!N(`e{ofoWl_)Jj}IspO2=pjNOJ`$eu$Ea!o> z&Z7^3iD%exN}@h+OhJ86kw0K>@f>7(g|Gwgl0_@=3NYKcHoAf~r%FCDvuU(M7tw%4 zyDG2X8sMLwx*z=#jnuW%|Bu1SZUZ@EiZO3fs$3{^%>jI#WYAUiBm&MT)K?~>uu%Cx zB?heWd5JfySN0V6VmuiOlT@{hWGW#crlN6CQu3u@W!4Ck#hKJ}btKMTK&8V49nX@lo3pz<>bu&P9@OnyGVd?h138ELqhnW(WjqfZ z&8swW^gIQ`Jon@;N3yz~F|UG2Pzgtoio#%y1Eu(_<}RbUAG{G6ORDk!#&L|xU4q*P zh!t-p&|V@s2oej7gj1MQK)4-*LOyPjyAJ04|r15a5heE6vvDT$>)b zga+#%iF3B<#0 zone=4wdpsqwnpN;_FJB;tK+lF3(3W3w&!Th;US)WBh#I=cji2ek37By9-r>%m>F2# z4Vz$TcIf&L&_%88=_cuS>r8MN>`)&%ypJIZZ(VhRuGMhUD{7NC?4Tidseoe?YB2p!%oQ9;$jxqFO1pdGvpU4Q##u diff --git a/custom_components/extended_openai_conversation/__pycache__/router.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/router.cpython-312.pyc deleted file mode 100644 index 0211d0d1e7f7563731c162efdfae1279d34e5de3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2544 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 D%W$>n diff --git a/custom_components/extended_openai_conversation/__pycache__/tools_builtin.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/tools_builtin.cpython-312.pyc deleted file mode 100644 index 644219a3bf7ac4450001766a8271da2e6d74118b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36755 zcmc(|3v?UTnI>3;_ZuJxKEQ_vzC=LENv<) zTitthzyB7h08&Ls+3q=8ByQch_tvdj_wm2(|Nr&x?RE=?}>H{*xBm0;;bLGh3$TO*x`4u`wYX*u*>h_IWyI69L@{7``zLE{`^d>1^z;Icags+ zG-yB}&Ul(5QUmxD! z-@wW{hBt;={4L>De=ECo4sQx?_HPbv@o!<*uHmiWZT{^no;SQBywksv#ofcZ!n^&u zSv-GuPxvwaV=S&4ZVT@nK<|dz{T)d01Pg{c!~6XE!u$RE!(IL^mR3029X{Yc5PsbM zc=(|IVEB;#P`Jn66F%%e%ySwpWE`57hcSB&eeEq?y7C_l7WsRF#s0pa_hrt1ELakH z;-n^6`m)adJHav*FGu|Ar?}v%KjwovPk2!wQmRn; z$zV0=ddf^=w}X|h2|Yoz1#9tJjb9yVvIXmL_tP3~KNoCxnG3E7@o!;%(c%9FOI?f9 z#?UvE)BsCuLTYnP>Ni>HI;5`8N$qE;8<4s&C-nqNZ9!^lPU-+l-GtQ5IjKRGx&^6Q zb5cX-)4*F&GpT>SnG^5NiJ!=c59GvyIq{Gj4{m$S=sy`c6?!7{jguPc$B23|Br2g9LYXgJ!B3*(u7Av)MU+{4O`oD=PR$B%Rd+Pga2AMftjj}m&6h=i_d zL|0qn+{i%N_?Qqnd?qx~)-76Digaqv8JO;OK~G z8yFpVE+j;xD??}`B%C@YnuO56s1OVZqD}}!qM{)(AoQOJ;jyKGetKNsx&Gn7V1G0e z!2Kr&PXz{}p)j&Nz3oT4kMspPd-}Tjjt6?t;6rUlFKj#3clc0SU-#jjK>OjIeck&5 zN80+j8Z4rNE(2|SeMh_Z9_#BAt?c?xXJ6Oh4$;gmdfEpXPiU>hovZtCkk)Oq+=pP0`|J<)dXSZB6~Q%dYS+Px3$?L8>Q>79ZwDu~YBknr5#K&WRl zx^HxBga$S*(?yvsL$4#zbt)9ibmhSCU}z+YG(+!zFnESyroK@4%rKtvceMT+v|<5k zW#;X(jW=|lQ@eY_(#|K)*436cimw3TwCM+BY^bKvP716Vcf{!d^kn5FI_3HP0!pI$rx!bNwi$VE^D#&HXeV zpK2Z$4aZotss9X8M?%qtzas=TKx;uhbchf{S?ob@g`g1+JO#Wp!SP&->*cDsD3xIc z)>z&I9|c^Kj-XbCkum;7O^}=5U*OO3%Bs`#2pSX=2se&*oRC*S1S_G{HyH5=p=ZYi zg;3CUa#Zj|PltTcO!q}cM~8hU#-dbaWE7_n4F2$NDA?Sf6SXHIt%3tJiq`J%nNcCi zI#sAaN`pqq(1xBLh|rULzI%HH#v)j7SSlzyg0YUQ3q2nVjRZr%fYe{Kij-Bjj-DL} zoERG%jt-7ApE)PG0>k~|=K_Q5Spj!B7SdC<%zfIC(rK_!xiQ8r93W*_;=V)5~4wdoZ)z z`;PUrGq}+C_0D!ciKD%u7K=mJfcwy!J8%;7PG%-!AoP4_0MJXw^sTUsvI}T2-B(0t zL*7yRB5M$w=jMyoq>7sp#mzHg$>L3MT|wGd95?#r9jm6sl8)NAsWxpbi|fi3QJND? zC;B6yz?d++q$_9|vaZ;T$Q8V-P#RUbYKX>r*^!;Nc>PSiz@OvKX&SUWef!V>8X*Mu z8tNAYPK%y4|FNT;0clkegv1K&Y2bx+)Ec4Z_@oy$4MqZNSqU@=j#DmSqx0M^jLz@x zz0!TTdv4Xn*{(UyuCy~hZFLEIk&B6|1IX?dEurTy2?H!{i1rKUJ<%AE;FhR6-5-ez z_p*2PA%fvo-Z#d@_?YGt_ZIaE4xT&53A+#h1oLF3bTkkh4FESVXoj~nMM6=` z>QfQXE~f^b37v}whj3qn9^><6UNT==6iaFlXgmW{)Q#XgcgMo%y;H^0yJl-{YCh5J zU@ywBCoQvqOv6x=<4pLm*I12VSo#`KW#E({`elq8Qf4l@p9L}{(7`?Z5yw4+`zCY~ z`kXG&N2$A{vYA?irkF0qKgUH%qGmZSra-MBtDKfS$9Q=S3C^fY)pId@Oe41sU9Tg$ zW66{6e~Wc7?T|BDuN;f%krs<~V=Gjkd`#;xG>VlRtRk5YM1W?huWh?wYVUA^ToNjtZ-L_>F z?D_uE7!&PKMYe<-i)n)VjV#>cqdqx@;3sNVYfF_}Mrn&KrdzQs_mxIlbdS`Q8u>X4 zmMi#XXtkW4JrlB(`>+=*LC?PHyl7)gU;sSY*l@JLBU%77f}xZBxDx2S1Onfp9*7i7 zoTH<2U)!;ReF2HN5`2{BmJ^>)I3Qsa-PujKPB~X*p(MVmv#0%V2iQ>2CFjd2!XCPp z226O0DzHi1qCmv1Ae2%1S_(=jvur>J5erEyCZk3ICj~|(6Gz#gXTVXkvRsLU63rl1 z#=?Z!#XO{sjtK)Hg}M-3XZp_#kM;+ZlnC``7I2D=01=AHA zUvyq@&J{P$nbxIiH>Yab6SeJWTfuZEAm8c^ev#9xwoF>Te<)$9S}<{_#ChF0xAjo6 zz9+8pq>V*!W3^IA)10X}UDKMXX-m|!$(7XZrAlh4l7k6T(1rlW zdQiSO?8A2qSzvtL#+W`$7=!>AKO>fqp`VKx`*T=5kUtrKE|lbu9*k&->2ioGg$22x zeJiu5CQKv6Q9=&vUeb`zpK}yht6?ZxL5T5N(*6;F~hbJLcvN9aDvE)cj3PCXkUM=vZmSGO%3Xc`Db60A#t5giNDj(FU_XuO<*85)Bd)%6JK(j#3JxW!oPN zDhyX(X!L|Y11uWCqYzU?gr{+VdKd{q4U`_4ZB>5cnZYvw$eK=L!43{cQzb9Jgi~!f z;wqbzB+08pgY+5^nm=smQXp|XMT*dAX{Ew8uw6}J?7=%i)xcQFN z;I>>a+;MU4a>_3#&&@RJ-Ihtif`hXO)CO zPfFMGv99MHV^&_=Yuj7MzvtlhdUWqO4Tuk*waOs_|3wVn3Xbuu~cx1Hk~%vrfl zXNJ@6f!H;BP%~*6EuKKIB8{aiv>}DE$W{?lxSK4mG^nJ-xPWpGG0Ed)gCm0^3LP8| z`D7m7CyN0@UG&_UP)~ziG)N*9QTyEJuxL7(5$gzF$8$w1vATiC!0Ax9A96cgBs6?d zxPY7`QUp^VK#vLp#=WwLOYVbaIqT2qrfLM|x!a|yQ>9Ib(xw}cw`131H}@siwSUkX zZ|+W(9+=d9Vk(vh)ZEUa<-Tb4enJz|T+lvKkN)OgvBmg5gx!nh~ldx%A71_5lGXK>@5dFLs}wVyvD>>gi#-^k`RwRz|8=s^E) zWOuW2#|Km$Q7d0Exbm^9_iD*h{#5AJv5$i9KmGmugsb{0;>h5j5ynlZ6o~&WA+1JjVkxv)P+`6R#pcKwEBW z$IBo1GXD-0Mo3fwaJZl|nzyF&i&FX3iTvv6?qq&*+}8Z%ywiR0+=X)&zkT7`UvPT! z*7-8uFI~NJb=y*PdlGefl67r!uHLlEv#8gWJ0=eSnC4eaHznQcXZz!}Eq}jY#Xa{T z#2&ol@^IfTZ)?!L)6!O>eYZxBD8md(L@>(y{}Q)+m(LZ#O!CqUjcKy8PsWsjH3xGm zGysH%+79lbZa{OI8_+xrf$#)~^dQ@o`DdC}%sOsDKcUMJKPq>|^y63u6NZ>UXvu=0 zA;LJzj-WP5Xp$WrsVp9D!&I96Y>#f9FwA9}JxsJB1E_xIPpV)*-XOC@nprU?3Egdm$paGWCMi zLeJoVqA`;qx|d{8ry(;uh|M9$G%*J`AFWn-z2>X*(|FCJ^yXips>nG6SiSRBN7B)A zYx|s|XU^I)@AgfXB;Ad1TVs0f(Z4S5ojp5sYWkVvs*UmT-ngj*;k>)x($K5x;x(HS z?#+|NUs&Dqw)~5SE*wg`_RqTtFP(kWc}K6c6Lpkd{l?~*xIbhpH9Eelr8mjBfk zdTv!8&zPql+S<10M9UbY!$6K_#-dATe8NjF;kQU+qy)mx@r-`ejH3@pQlG`yJ;Lv! z)OABnpPOV@$Dp6^L*xt72qFY1ST|k#7T-{thS2rWZ9j{<=mO zLsn0NT?Bp~=?{kj0nri&fQB6#rnoH-cy_FRSjv%rHum9&QV@O*Hwu49K@|mWP(Z6y zAPQg1YtPxU>88FBu2UwxB}4s^u#Wmipy1E&iyTGpGMBDynr?Y>`|I0hPW;L4JDf)U zIRAyotlzp&XwkRM*cUmR7K%B8XF+4qSKM}#F6eQ_=1xIn%Cj-y+4zMO*Na+|_?${e zkE4eS;HMl4NjVHaMC67{R0i`h1cA-34QfKVpf;o*Qjtl4K-7VZG(MyZBQ+mV#(;Xv z4=Dp;6uKr4ZjT9NtPd$;Mj6{f%2-gw{*W?OlyL-YA!pDYa-uIBxON4dp}deg=CxDtI%yATd-_;u#c>djGX~N!`20nH^RTB zV3GntURZ12!$~v;z>8uRDG-q8>kKNP+r}%@`$ZwJ{}zQJbqE0WJ~P=b7!#)A*YdBG zTrGK{_syqXe`R(zEi&X$r84459KoX1* z@(*f=x)jR6=4TK!GL%Ew6Tr@mUKSxNXV`loq6jhymDNEbx$JnvI&?}qo6*HD1=kCF^^b>NvT62uYS8aa#$6GXnT=|aOIcy{RMtM z!0%i5J&E5A{7M|8!%yR@R#<_Vcl=*aQRMfqAQ->1;No1xQ-*}IVp5m3I^+2@ zA6skE-tv^UDdBAbYjElGw0UNGe9L}l7?e2favG0gfz#L>3kFCOa zhZnE@*a{g8YHv(<8y{Bu(youKK32LR;ca+m>75^2D^U7c#np<3m5t~BRt<^guld-z zhP9+6;ca<9?I5_E+imEqFBeP?4ev$1g+Y0-y2Hu;kniAi|DqDHca409 zR`;%n#T)nzz3$yL6o1dkcNlf=*;u@Z?=b7$Yo_@7PQJscd*8+48~6^p?){A}q<^60 zDb%wNbs}zNA?iWgPN6zf{o5>5)hbXG^aO*iM(oC@K-D1jbX3!I5NM%3I^;71uqM^A z`W5QX%Q#J`!Ms8Z3RIY|tWd%z*ENyLe44ZSUh*@9#+Z3X1?C1#at&ZoZ39q~0F&wl z=CQ?WYOLm4(#vy~HLrNUx|&fAtq3#qejE2 z?*AKNM!EeFTh2~+PCDx2)OP;?!_Ck05wNqTAkdeePWrzqJij3GVmYT6o)a_OFw1u< zm{lu_!h@Eebr3p28MfJWmPF%r=z%F>Oo+G|Cv4GTx!NHT(kwe-HhB!7m1)aTOWE{6 zen4{^g7^vB3%1_OIV;LySi#;S8iG)l92^l2qJGhq!3u20ifxlIj#V`FV>b)@plA+; zNCObjdwe69>|3~t-736G_ZuT)0|TK*q))hv%La$EWp<1T5fKF<>&);VGz_3xFnC62 zrX(oMFr~r>B>nvn87&F5bPsVQ(CB1JnUGx#Gc7dXHOgZ3+q3)@!xW-RZoe#{%Rw8r z=r(#w)>QUh!h3Z2J_X-L5FwN-;gn1$u{ngZ#+x3DTI7aj&?gkgxyTDSn8sZ-U7yHX zJ84KGwk_dqnlz@1>QY5(6Gdy&wvu>RW2$U@qHO(a;Z1k4Y$q^Iv1QT*Ohar@O~O+% z{bYRO-qgl@iH-YieJi=KH|gn{G^gFGQtqk*RheW}dVF%}q-VpV`7>+5r9r4hSZmXb zo8CTr{qW89WaDFhUEDVP>{Q;hqN_!J3lCl(RnJte+{IEt+#~n|D`Eub!z(x;Msc8`CCR z%H&I!eAAxkbASBp*}d_tU2~@H1)bSkn|2poX}a8$a<56a*QATe)5R6(@`eQ?m%nzA z)8zxl>VRHZ0ETeYR1`=#Zmap~iwe%WmtXK8_p3WL&Q=5T7!l)v=kY|nHi5b;VUr<@f1H{iYiDki_nG?R3_Jk zy<|0{#t)TcOUSXIRdOtQj{4-d{5C;jFbC~!0-5oUu`~}=%XMVW^4Q2dFhK@F_>-T4 zKK=jc{l0AxG(pPPNSpn*s!beosUr|8tRiL33DiOeRU@Rh&YbRlNAT%Mh!S-Ox!roQ*HQjH`_G(1r zPm1{iV*<3RU`wLd^hAU-N=67I$_R+g5)vY@!g%cpNNA}zqmytD&2)x?pa@Z3d9CMa z&&;;?rmn==?qu14xXBA@ub6m0vnNX`Q#{ z&D$Io_g~n5@!*AnQ=V(3S4&ggbqVjfxefc1wyw0znJy^1W7Ih;3&4*%pdICK&R4B| zv-$Pr*}R_=zEk+8#mTDeaa+Y*Y+mlTO- zddJP+N1-`y-{gU`%}x!w)G@U^;i;Xot-fog!Y&TfVE95fZv4n>LVO89;J_0YAvdQG1S=77 zGMHSE%u}{xLEfnog4!%02zh{d8C_5>1H=jQ3S|sAWh^U{0qR!jv93@Ch(al2%Wg}n zDQ3uMa>AY^dV=N~7Nu7l;IP#st%jIA=AbR=pjAOgh84}eu?DG4Ae8{m*P}4iks<8|y1VOg8oaZZCeKuE(9Ksto zCD}m`GLe`iW(6moc~b=yW@HExuH~p1>_* zk6TnBwX8t?CEP?otN?d{yq_qH5|C#{tT0+7$K@Uq4#zx0#I-LwVuflNze5enis4dj zDehQve{ReM@`oGoLZ6`HVnEDhe47j0~ZWL8A#O2nJf`BIBNH)lAKS zX%URO<#uG#Kn5M6HkpY6VNZgIl?{7u^5cUfM*?vJk|X=9{|mPWcPaRio~p^nuOj`h z(h5ZyiW%Q48iUaQ%!Y?SA4La7M$ZbgIR&GZ2vP^6&2We_K`gOC_6S5;&&9Wq=3d)SfJo@3cjFV z5rL>hABaZSZ$j{Qj?8nUhW-c2gG?_Bg@JRD#Ox~F^A$_S_=aKC&6*P-VlkH@%ZW?G z*uH6`qS~9N0SA; z)S#j(J1*~-dVZ#4_Ee%_SA0)zGQV%qbUQ~1HXd(jpEGrQW-qw-^o6HW_WFdqekOP` z7`N9a?d=P&WOCd(J6~FzDqWu_T|etdmTpd!9!iuRns*haT)u?MmvVI^Tpieafl*T0 zRg!iUvk;f{?BADml`j_A${;|q6*?yO-PNMbg;iX`S~R?}?#;5-%l>557o5l3%_nT- zliin|OE)&d{;05IYV)hZb2Z!I)%f2YFWfP?KkfB>!MW^(m-O@b>yo z&*h%mjT=&pyAzGOQ;l7T#;#=JfpqPfH=lg{$zN>TbG_z+zK?2C2mFZx{$%HqpBr`F zlHX_z1%;qP3w_i2H=>^uG-m60yZd_g>`1b)lWI8r`tg~w$=WTfnp?+ye*E3zAH|ZL zPh~4vC_sxAsyMsnV$X%18Sl-riPHU_*t!-P=$#fCIakr7W8rZgZ~qm0`Om3W{~@vs zv+l+AUETHk`+M3f-KE+O&3t#U;X{j#;*Pxy$ocu!HXlxZSs+gS4MPOR8G}Q`3r-0dH0&rR*;rbZuq34lZqSNchq5xSw4;qrZ9kgWel*_uJIU=| zPqlu1(vfzSr<_#@C)@nfa+{8WUMDW?MQ=0z1HMuBT3++g{v?RYNNwWUG71d*5aP=Z zRmrGMt^d^2NMtm5{}=>$=tz3Ak~%0nC`L$v9U}xHb@@TtHbD)AkOovUgsRPKtQ(Jw zJOd~?0#y_RYr| zA25jQX}MeRKcVsL#cc|NExB_1^6^wbQ=*`0W=PWVPc?TXn!Az(-6>}`w#QwCulLV8~<*pg*rN4~^i zp%s~7SLb|k#N@D_rRB#CwSmvcCGEOp2ZXk4-&URq@)=^*e_XBFTw$wHt0_CB)TU4l z-#~q_?1(DjVpYCH5#eTUBe5)apOrQna`?v;xISgSc!hk3RZlAm2lmMUr=`C8<)TF#{N{z_C(G0o7-+ZmDq7K zS<{;==$o|6=a*dBeR=mAb*ZYYiK?wPy|=a}w)G^d4kz=EOqxC`C{GovNffM^X$3)) zD%hVWfElqTWvfWoDpIzs3ES4R&6Tzlrfr_I%?q12+CZ7ApYBZB*2q_NNn4c?pYEF8 zp0qVD=9vpEcR8~Q!X-1LG8D@g_q12E1AkWX?W=X~SLqR#Y`OELfninvfiOr$pr!%H zO_YOH7yTBWM?m&8=VABIn`6hWy2YZPO;UMZ;$uVM=U(J_U!qt;p+M~tZcy;ADEK`JNbJB=l!dn`#*~u(jAD>b+^1JYs8Lcb z`#Qx4nh5kpk~EyiA=Y%!p3&(X2n`Ph0@r!r$H-zXNgk63rFW<(1%E>saMQ+p`7-w# zjYf|>Y13+bHPd3oIY*0m(Ms1x_!@mJ%f)#~E?pnwo%%wSi}R9Px~^KS_hu^gF3ChX zD&D0pV!1dk$))QTcyp#vI4{Yi>q4`>k7xH%WLY*63JdfWmW}h0T)J*H>DNtxXTj-K z?;@QRJX(Fh)UoNlnd+IyO#SQMNR+qSg?F9;X)vkV2Jll3nzM8)b|6@yxvl^0G`E>X zvI!c=2C-1`F7-f-WLhWmTO{Gn3L43B+j2I+U^u8*qCpHNN^oSQXb{8CkMJYZD9z78 ziv1e|D%!#7ZB(>_|0}H!BKW_|Y6s7Gn?8{jKf_%=ME(+bitzDE_$|_|%yPB_v_VEE z7~9ktl>4GYK5AxpK|@f{ z)?ze-kI@jO9?}bXb`1MwA+hX14yDHHo_tXD)5nM=| z@)Z}MKFeyukk{flqN?#s$;^!4GYQF!1vp zA}fF&1v#s6vROS7wdJU05TT|rnNtgGb}c(#6N?_BBdaYksHDkx*hAMg=$05S%l@vE zmL?%C*U$}1(;g7Qr9a` z)c~*;*^4LTK-GDh^WuRE2R`!@T{(F9V7zie(zEeLdw=ryJCDyxNz<#6p7lSfS2Vh-SHc((QJ{XlsXtD!1)q>izJrKu(cV`tF}D=psG=mYXmiL`IiCI0L__LFEa{m z(Dw-N;>xj}+$WsCgFulzgOkKDy@}Xz<1$Gr-nU{eWk5$B9>l!u$qb9DI9?3>zJ`=@ zbHce9tU}sbd9Cbf+4RV4DCym~pyym$7C9~P3fRHTSftg#nHw#7p@mwxd?|0DL<;ou zuv(O3OW6#XqR^TlIB;IFkZ~<_I^Fl*eI0v_!dJ_qtzp6q|DV>df=Ja6q`bb=5amPG zt->^A*R9&7T18L%(!JzjvSX4m(shNpR+39J={YYJrZ5a zLj~=C!8uFd`S7p=uPRWjSev1nm=7gsmkd_VkwH*kfuRVt+!z+YoMD_NgS|7jZ@Ed( zuc>Q(}%2X#z1|Y7b`SLt&l>wr=1o}Rns_9l}JbRO# z9h2tUj-r&KCgG?_I##DltK%lPlJllqYZ9(CNmt{fKJBd_%f7jqElKazNylxgigXTuacE?pp#~Whk}y`{;rtESin6Xxf*_xK;<_TK$T~ zwPX})3>rhGAUw5%EFqI*QW`Wbxq>s?lcpdUtXfsd^0>*&_^OXMjzVS-P75z1OhWk?UBH6h4irHW1|4#cAB1t@uVey*qyRf zC2UnGTSLOu@J|kZiNR&?*Mx^JXOU&73Mi_{-DX%0ld~-Iqs|i&%;(%ifN0sFHh*%V ztD26t+}``KK#r-CA}9q!$F3G_Q$UFa-42s5i<;q6jyqAn0y>~2%ni||{;~rmWyQ;q za0zv#EM9;SSPbUyJ1N}?#hP7o9kV`2If=@?@-?Wj;67^Y1ns{mn&(K!9MLj4rr<0` z%mIAmP<2UY2fk7au}~-A2lnl*WcKaIGw}!%ZyB~|a52Br41Wo1DOd+a;U<1?q#qWq zVxBaw+p|eR2u%?SWG8{W?QK1Q?jHCwh7WepC!w{BGA&eCg>wOD!~#7Dp_4#UrzH_2 z+vzBwddf2X(j}BB`3lUUvu+fTD1Abj0=~q+H8>vuB?_eGDjY-rm7LJotluK+(6Cp} zs28&hNC|0@0D@6#Y$q>^Iaw?ReUdOj1qe515NHprb25UGg%M`&oT((8=Ut9Q=L^eH zh4qQT`c&ceMB#QOu_%C~f>dBrw&nyjCXBiw`0@kla+ahV+Y^rMNypBVX=mKD^Rwcz zspCIzPTFsmS5J2(%A3C6jOHDahtlP1QsvEw^5z@olI7bbJ76}F@@z_YHlYQx8_jSEiHy5Q#A6;o#ud37*W zS&rMiaadRtRHq8M69sS_TR5eg3Zr^b#UQj!7Ab2v^R9al8tfO_R(ELlckB5MPWNs@ zn+q53ZErK;Bw5@Yr9m4(!6ln1L`4!+xcCABY^JPe*e`R9*gjF^F|#m9Sws4m9&1Ri zMkT`Q5PT6SPzhTyz`>ev-!Z0F?qljrKH6Po4(4gf`iR_X?t$|v2OR9N??7bsRM>RS z&ftu<6gIh)cg`W*2yROgP?~o~&@baHE6lqGt)DTPjE|!>IE!*{c?Bs~b;4CW-7(XY zbZsR{ZFS1KCgEK(w{~~ZyN75sHl7b8(-`G6Km#Hl$^g~laTp*?RL+3JeMv;6|e-9^*mp>tCDAE6FeD4qoX$MGs~ zKTfX#B^uIX#Xh{p?Dv?cAsM>aOL(;Vi@WEM@a{3ZZ-y5rv&h?YJ1&zb(g&Azz zS&=R-yJo#=o!*x$UOUs7DBd{Rnke3SS%15*pZ}UyvoNX7&BLi6Py$H?fcUQF)v|G9Nt(6^l z+V`7%2tRP~2pK~OD{UewA7!VNK!`-Se4SzhkOdkAK~;FapK>VJOTAANcvj+9o7wop zIfo}*w9<7Cwnf3Q<}K)P#&7}2w@>F!bzOd8!AL144pyMrsop8m<=qQrO0jSj$D);D zRfYP3>6Qf!r`hPu_FMTkyWV+$U4B%3mr^B;ms&M|1}TTk1upg>X$i+`VjQoAalBe^ zynINXaUBJ-3+8{+qz0TM|D%2j<6Eu3nP$=3PTxq0z=Uc!d&F2*bkaAcqsqnEwO&tj|hzh_yr6>!n=#Z&AM^4B-97COwWl-0k zZ4cZk(6+%B07(QzE67-K984b{5S=IGy*kMq>I$O5sujmUVN<7R#vZ_nFJV)2cMYI{ z>}k9)@b;iCObXUCO}repbR?5)sPo@73@7ko7T&Y ztUK$hz8e;SZoKx1e(B>{zJA~RLys>Mk$1Z0@qD-<1bTt{L()8SmZZBiZfpGv!2jUYgYkx) z$+BHPjl@gap^ZLYwqdsRrs18YWZ7e|LA>qkj5~cPXJf+Im~yrxoGtOzj-<2mRw(H_ z7&jf1;F?g0oP zZGM@BtX!Y+wIqBP{QXH^7j6=rzVy0Hm&`CZ{n*vGkcXNVJQAmwhnwz23eXZL;(B=~ z;uaZxT!h%W4hupF>DyG1eip&gd{@=BQbqNIy95fZQ76!wP`s#z zZVw>3A$Wp5Lk4aF?7=YnFd9`i2bt)Zf$<$Q(bRVUE7Zq9!Rh~rrY1=NR>tW?Nl(L# z7Fh|PIO$n)qkYL$$Aa;Gq`pK7Kv;tfGHIDTx*KQ`sw{P#OqI0Yx7dsz$8|C&GWz&j zkRu>{fw_6iI7D{9A#%n;Ewj~#EmMJtFXVaZ+G?~% z@`a3V^W|G^yH-J!$X+_NBVntX+4hO8^>ZzfC0&7++yy+e1%7~A(7x~CBw3eDBPKQK zn-boo&r`_{9{$(5`dvzmJcGR*wE2T zTRL=ELjic2hiFG;=;av?5Aq74W8S_sF9SK=@9i7Iti6AjNzlDW-1ilH9Wn+36B^nd zApS>McEdfsPU@cDM>@Lg`#3QzDXEX{nRX8Mhff6icaOI}w1*_O&+zR$0@uJ;f;pp^ zspQwxht!($+*I3C|Fre$NW8ph=4}7Lze_k3WuC6aEjjZP#0SmO3!3L-huxMX z4!fBzVR>@qEuEaO8W3oBROV;LKjsZ(SNZ=lyKE6-O=z9qeOC(!D20el;LuElj` zqB`<;G?E!>YQz0!q$!(mYfnuSrcsUX#|R{X@tYZfQ7p+$+~s{fzTvliiNB#1Z>APw zVwYD!gDK%!7vIp9wCx3@u*w48*mt0(mk-4~yvk!o@6apYf;4nA8RlaN-$BF1CVMd= zOZg6YQooonL*tpod`A0Q)P^IcB<0r33bhLTgZP>xGXof}TH(!d%KRO@3JDkSCJx_p zzVvy@EFHU~nF=`lXHqi}N{6aQRat{_DF`ZgHSSnVgD&U$5enCvtw&z*6VU34X|Omo znZ-G-KRGxY+CFaDL!XY>C5!@x-Nt>w-%#+k6#SZk&nO@%X7+{VwPi38(@^uW)fL$MiL;h!BHDX3=hxY?_ER28&^8&%~&vlS2O~`VyF&j{5uY zMMf9x;pLW8u~QQB@R>BCsj$o$N*jrb7afTpSj^0csBHU_gqRMsFACym@(Kk%uf{dyc!>qXATO~&3(+|62TPwau_Hyj|E+S(LDLu~ zY(wot+$}rgwoN#qw7JL*@-id86`tRC`h=~ zTAB4E%VO+p&Px7eGx6nKLC$eGK0$0ff`AcZB-gn4Ti;?JW@2IqnMa72(W&)_{DI~Q zSPxajX6}MAi)C9fUO0=|f#ry9mFOj+_$B0a{=6h#`xc6fuU`RBWh5;3)2c=Qi3l|d zWD!u=;nxq(wgJo}Yr4*>kgETvclb7Y2a$jFpY{z*^aezO;SFXxQmws-R(zEx4_>-K z*KKlSUIX$<>O+AV!JaSb8AUbjKFTyFAtz;nMmu?gseq^lrEkL%pU~hI{*Z2D`|rO? zu@@=$&omqj6g)%0YZTDF96Ww7ZoP$>tdBqp3*+1|fiz`E6oxO3<(SWyPKKfbr{Ot7 z)Q%6H5v5CV)%zrhHsn7Fn<%LL+-_u4JNY&%O_?g;cXzsB&eZss&2#D4R7cWQDQUmN*Ab0B$NjRI*g=JSpFOMb*>n>^MTQ<** z-5mJSJ*lGh*9WJAGcf$08n|js7Pa5CWdm+e2Tz8SjJ`xom{s}5X0j*oD*Ohu=AubxY;I`E0>aeOZsWpC$u?|S7* zzxrFxfv*-#TyY2g)uM%~c$~jmM9-oJ{zIe>WBBskP5U<|SF3%$&bH5Ec)z6; z@ek_k`@E(PTD274VnLY?`3;DFxY53^!tvoAE8;)1Q2Nh2c@(eJ?`t;xY>g2ql3^?BA#y2{^JP`()p(RGJfB9Z1lS;7#%7j#Gm9iyPMTOalV+)XW%W@1?RYj?z zG}9^gk2DPh+>sTf>hzYI9ym&dSk-3GTV}QJkEgeMRI^B@MHjDMymXc)d0A_9qlq`lcH z)dJ1*J=f7W`+_8HWnWN}KC}k9PWqOi&_X56V5|=^gHZ_(Fr>Mk%4le=Zl@T70c{lP zqTnb6Pg6j8&ys@acPRD}1@BN$NB5GA6I0=5IumR+g~0*on~G1MoahV$`bS1a$<@sN6)p4%+Q*PC#T=_3K$1gcE`$xq3E3Woa zu6RMGmS1soajx!vvcafv`7i&EHWBkHa&I?cT<)7QD`NMqLQna9@$Zfl8K~E7><8&_= zS=7Y23l_{QYT@z=7pyGm;0%^UCq?fZan21_t@-g)&2evY z!h;r=c+Hm!gTErO5q|vHAMUPpW&OmmC0W$kuCr0CG>zjVhT%$w_E=P!C1jpaqgcE{0Jc$6TADr(Wbrg%s964Fm<6+u9P~)V*}(k=$K5 zbuXo$dBb_{%^S|lH*faOZEYz6&!1i+=C6VK3SXi}3>a?bSHRpQIw=yJ(|L<4@kO3P zUa*9sz(%ns!YEp?QoI;1C5j1{kLhtMS(1uUDOF6B(#3QsQ_PgwifyHAG0PF27~>zEvt`e8Y}Jyd$1ciq)YKOY zdETM2u~c?k(_R3^Gbk?b9LI8pzeY6$&QGhh?RY9~c4rE_FO1q%KRF3&)j7-f8Z9Wq z{oL5>%sFLZX7udTgaRk{T}%L+GClfZ<>KhIsoBwSzr7*7GC4gld-aOnF+Oo_^y<`= z*1h{9+4I{wTPH#dTJX666@r1=Sp?=TF-VbvXz)4*-j{S9ydfGf@QZL;ydUl>#zB(S z#TH2dBpE%{B1!6PdK`935^4VFS@5`6OjVPGoG*c`XPH-P`=``emAP57OvCo%c~w(& zV^|9#!w!xC1#7vT&%lFs$xUuCjAdvR8@$F2FU&DQqWIn@dkH; zED6`h4Xz;0_(Dl5*Y-oyBQWWvi-^!q$w%FDg$`d>Fg%)oqbPdEPb!AZ<|(?4W&)XK zmJFxjQ4AGDoBfPZR;!ky>WZe8{j3t?kWzB=ie>m|g-schI@FiUQrV%NWzP9+0UIro zCYCgV0p1r}kNWYc+4JWoF8MLtn5!%l1YdLwYu-;dY>n#+pwPv1l4beXS*_xFPD#-m zP;Wy5yRTwxuysRMoHBfc2~+lUgSxCLyvpi@0`5?PvNBwT0--3TZF-7Q>u3ODIFj|i zLGGtOR>*eGz{ly2(x1+5zHxq|=bcsY7j2(Klam`gKV-7L!yjMx=)&g9r#H`B z-ROIFRoqM-*gllUv3L8}5RN^Ma-D0-A1-faI_hFf%0J8xZROv7kbnF0qZ|2=)$zY( z2EKWi>v=*1m>bA{^7?O=R>wCpeGfAoYg2co)(?FDv$4(0TibnutK)0acc%YcPr%A& zE?WDCQwPs>lP|iZv2NjuL&wM3g+I2(fY(|@to+MKls4uD4Rnx*`s&zKK+YlCTIih5RFqhm}{!4D; zldi476AuPYd_KNDII^A`*@1bo$9z11nU8RMk@=SiMHCe{+64nW0;JWlbW85mx|_DO zh>znItS!vCt(^QIC$A@E`W+DcKlXIO{%_fn2=P_h*R&-Ry@qxm3L9QpPxgJ=3Iql7 z$`mj!@`}qZkjtc5trxMnKld~%8b@$-fv+9EWOx;7v#SC@B)erpb7g9%dX?SH)L1Y9 zz6@r=wHxpavRf%5=4I2B!+IjmRXka(0AgXC)>H=dNOJ(CRa^^KxC|x2GD?7FLzhik z_C5glT*b8XVR^=B>~rNCre(=NFvk5}fbt%%otaoNw2EiQ zYD;)D)v|`=u?nT&M5_vc!tlA^OmGQ$|2jAgEAz0Q!*W=$8$7S_Eq^r-#a$B9^bs!n23>R+x1N`7|q7!**EV#E9_tDBnl9zf{3GPJGl`|%)T zzBmIH8+_E8u*etZ0DIAkVA@{9$L8U(joxZq7GUwcy$ati#OdpxgJR93Cy<;(f_fQO zNaIzAo>CM)rG)o0@EJwc>vi_ZT zCe~5!>x`uyW%t!%Fv6;?d_93&lB9ZeCEx;6(270v8$k;H+zwu#cZt`yDsPggPlGDR z)dtV0n&$ukz;I={SFknzy@0&Iailt|SjR(a{x`s<~Kvu|Z zoMgIx`J;_=|GL=EtPJy_B}hzh7ySWj!1&Bp0MZaEiq_NpC{0+-!UFt2^5b5$3?s!X zqgdhn81zA4Bx~|m2gh1FZ1-3%7c_Yi^np{L0>FA2XRwS0F++3dC=$HY8FS45 diff --git a/custom_components/extended_openai_conversation/__pycache__/tools_orchestrator.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/tools_orchestrator.cpython-312.pyc deleted file mode 100644 index 5e748b521a3dccc7602b1174644466821b052dda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16745 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 Cj(W2I diff --git a/custom_components/extended_openai_conversation/__pycache__/tools_web_search.cpython-312.pyc b/custom_components/extended_openai_conversation/__pycache__/tools_web_search.cpython-312.pyc deleted file mode 100644 index 9fd144d8678e8000fb7d0b02e1a455f6e31caefa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4260 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 Date: Tue, 28 Oct 2025 19:56:20 -0500 Subject: [PATCH 4/9] fix(exposure): use 'conversation' assistant id; guard 'minimal' reasoning; tighten Responses/Chat breadcrumbs and web-search gating --- .../conversation.py | 90 ++++++++++++++++++- .../tools_builtin.py | 4 +- .../tools_orchestrator.py | 6 +- .../tools_web_search.py | 4 +- 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/custom_components/extended_openai_conversation/conversation.py b/custom_components/extended_openai_conversation/conversation.py index abcb1744..ed275626 100644 --- a/custom_components/extended_openai_conversation/conversation.py +++ b/custom_components/extended_openai_conversation/conversation.py @@ -84,6 +84,7 @@ class ExtendedOpenAIConversationEntity(ConversationEntity): def __init__(self, hass, entry) -> None: self.hass = hass self.entry = entry + self._logged_minimal_effort = False @property def supported_languages(self) -> list[str] | Literal["*"]: @@ -267,7 +268,20 @@ async def _run_responses_flow( if caps.is_reasoning: effort = options.get(CONF_REASONING_EFFORT) - payload["reasoning"] = {"effort": effort} + if effort: + if ( + effort == "minimal" + and not _model_supports_minimal_reasoning(model) + ): + if not self._logged_minimal_effort: + _LOGGER.debug( + "Reasoning effort 'minimal' not supported for model %s; " + "downgrading to 'low'.", + model or "", + ) + self._logged_minimal_effort = True + effort = "low" + payload["reasoning"] = {"effort": effort} tools = orchestrator.conversation_tools_for_responses( self.hass, model=model @@ -302,8 +316,19 @@ async def _handle_responses_tool_calls( while True: calls = extract_function_calls_from_response(result) if not calls: + _LOGGER.debug( + "Responses loop depth %s: no tool calls returned; exiting.", + depth, + ) return result + _LOGGER.debug( + "Responses loop depth %s: received %d call(s): %s", + depth, + len(calls), + [self._normalize_tool_call(call)[1] for call in calls], + ) + outputs: list[dict[str, Any]] = [] if depth >= orchestrator.max_chain_depth: _LOGGER.debug( @@ -335,6 +360,11 @@ async def _handle_responses_tool_calls( call_id = call.get("call_id") or call.get("id") or name if not name: continue + _LOGGER.debug( + "Calling tool '%s' via Responses (call_id=%s)", + name, + call_id, + ) try: output = await orchestrator.execute_tool_call( name=name, @@ -344,6 +374,11 @@ async def _handle_responses_tool_calls( ) except ToolExecutionError as err: output = f"Tool {name} failed: {err}" + _LOGGER.debug( + "Tool '%s' execution returned error via Responses: %s", + name, + err, + ) outputs.append( { @@ -352,8 +387,16 @@ async def _handle_responses_tool_calls( "output": output, } ) + _LOGGER.debug( + "Appending function_call_output for call_id=%s", + call_id or name, + ) if not outputs: + _LOGGER.debug( + "Responses loop depth %s produced no outputs; returning.", + depth, + ) return result result = await client.responses.create( @@ -361,6 +404,10 @@ async def _handle_responses_tool_calls( previous_response_id=getattr(result, "id", None), input=outputs, ) + _LOGGER.debug( + "Submitted %d function_call_output payload(s); continuing Responses loop.", + len(outputs), + ) depth += 1 async def _run_chat_flow( @@ -407,8 +454,19 @@ async def _run_chat_flow( tool_calls = getattr(message, "tool_calls", None) or [] if not tool_calls: + _LOGGER.debug( + "Chat loop depth %s: no tool calls; returning assistant message.", + depth, + ) return content + _LOGGER.debug( + "Chat loop depth %s: received %d tool call(s): %s", + depth, + len(tool_calls), + [self._normalize_tool_call(call)[1] for call in tool_calls], + ) + if depth >= orchestrator.max_chain_depth: suffix = "Tool chain limit reached; unable to continue." _LOGGER.debug( @@ -446,6 +504,11 @@ async def _run_chat_flow( call_arguments = getattr(func, "arguments", "{}") if not call_name: continue + _LOGGER.debug( + "Calling tool '%s' via Chat Completions (call_id=%s)", + call_name, + call_id, + ) try: output = await orchestrator.execute_tool_call( name=call_name, @@ -455,6 +518,11 @@ async def _run_chat_flow( ) except ToolExecutionError as err: output = f"Tool {call_name} failed: {err}" + _LOGGER.debug( + "Tool '%s' execution returned error via Chat Completions: %s", + call_name, + err, + ) messages.append( { @@ -463,8 +531,16 @@ async def _run_chat_flow( "content": output, } ) + _LOGGER.debug( + "Appended tool result message for call_id=%s", + call_id, + ) kwargs["messages"] = messages + _LOGGER.debug( + "Continuing Chat tool loop with %d accumulated messages.", + len(messages), + ) depth += 1 def _log_assistant_tool_calls( @@ -551,6 +627,18 @@ def _log_final_assistant_message( ) +def _model_supports_minimal_reasoning(model: Optional[str]) -> bool: + """Return True if the target model advertises 'minimal' reasoning effort. + + GPT-5 family models document the 'minimal' effort; other endpoints generally + only accept 'low'|'medium'|'high' (see OpenAI Responses API docs). + """ + if not model: + return False + name = model.lower() + return name.startswith("gpt-5") + + def _ok(*, text: str, language: Optional[str], conversation_id: Optional[str], cont: bool) -> ConversationResult: response = intent.IntentResponse(language=language) response.async_set_speech(text or "") diff --git a/custom_components/extended_openai_conversation/tools_builtin.py b/custom_components/extended_openai_conversation/tools_builtin.py index fdffb188..562e5e10 100644 --- a/custom_components/extended_openai_conversation/tools_builtin.py +++ b/custom_components/extended_openai_conversation/tools_builtin.py @@ -204,8 +204,10 @@ def validate_entity_ids(self, hass: HomeAssistant, entity_ids, exposed_entities) exposed_entity_ids = {exposed["entity_id"] for exposed in exposed_entities} missing = set(entity_ids) - exposed_entity_ids if missing: + # Exposure follows Assist's 'conversation' assistant toggle (see + # homeassistant.helpers.exposed_entities.async_should_expose). _LOGGER.debug( - "Exposure denied for entities %s; not exposed to agent", + "Exposure denied for assistant='conversation', entities=%s", sorted(missing), ) raise EntityNotExposed(sorted(missing)) diff --git a/custom_components/extended_openai_conversation/tools_orchestrator.py b/custom_components/extended_openai_conversation/tools_orchestrator.py index 8dd0f00c..aad8733d 100644 --- a/custom_components/extended_openai_conversation/tools_orchestrator.py +++ b/custom_components/extended_openai_conversation/tools_orchestrator.py @@ -10,7 +10,7 @@ from typing import Any from homeassistant.components.conversation.chat_log import ChatLog, ToolResultContent -from homeassistant.components.homeassistant.exposed_entities import async_should_expose +from homeassistant.helpers.exposed_entities import async_should_expose from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -194,7 +194,9 @@ def _get_exposed_entities(self) -> list[dict[str, Any]]: exposed: list[dict[str, Any]] = [] for state in self.hass.states.async_all(): - if async_should_expose(self.hass, self.agent_id, state.entity_id): + # Assist exposure uses the 'conversation' assistant id; see + # homeassistant.helpers.exposed_entities.async_should_expose. + if async_should_expose(self.hass, "conversation", state.entity_id): exposed.append( { "entity_id": state.entity_id, diff --git a/custom_components/extended_openai_conversation/tools_web_search.py b/custom_components/extended_openai_conversation/tools_web_search.py index d5f1d503..524a5672 100644 --- a/custom_components/extended_openai_conversation/tools_web_search.py +++ b/custom_components/extended_openai_conversation/tools_web_search.py @@ -89,7 +89,7 @@ def build_responses_web_search_tool( return None if not _model_supports_hosted_search(model): LOGGER.debug( - "Skipping web search (unsupported) for model '%s'", + "Web search disabled for model=%s (unsupported)", model or "", ) return None @@ -125,6 +125,6 @@ def configure_chat_completion_web_search( return LOGGER.debug( - "Skipping web search (unsupported) for chat completion model '%s'", + "Web search disabled for model=%s (unsupported via Chat Completions)", model or "", ) From 1e668ff04d32e024dc690d429ea83e9839eb48bd Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 28 Oct 2025 20:54:52 -0500 Subject: [PATCH 5/9] feat: harden hosted web search and expand default toolbox --- .../extended_openai_conversation/const.py | 120 +++++++++++++ .../conversation.py | 78 +++++++- .../manifest.json | 2 +- .../tools_builtin.py | 170 ++++++++++++++++-- .../tools_orchestrator.py | 50 +++++- .../tools_web_search.py | 31 +++- 6 files changed, 414 insertions(+), 37 deletions(-) diff --git a/custom_components/extended_openai_conversation/const.py b/custom_components/extended_openai_conversation/const.py index e39be0eb..c16618de 100644 --- a/custom_components/extended_openai_conversation/const.py +++ b/custom_components/extended_openai_conversation/const.py @@ -158,6 +158,126 @@ }, "function": {"type": "native", "name": "get_history"}, }, + { + "spec": { + "name": "rest", + "description": "Perform an HTTP request via Home Assistant's REST data helper.", + "parameters": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "HTTP or HTTPS endpoint to query.", + }, + "method": { + "type": "string", + "enum": ["GET", "POST", "PUT", "PATCH"], + "description": "Optional HTTP method (default GET).", + }, + "headers": { + "type": "object", + "additionalProperties": {"type": "string"}, + "description": "Optional request headers.", + }, + "payload": { + "type": "string", + "description": "Optional request body for write methods.", + }, + }, + "required": ["url"], + }, + }, + "function": {"type": "rest"}, + }, + { + "spec": { + "name": "scrape", + "description": "Scrape structured text from a web page using a CSS selector.", + "parameters": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "HTTP or HTTPS page to scrape.", + }, + "select": { + "type": "string", + "description": "CSS selector that locates the desired node.", + }, + "attribute": { + "type": "string", + "description": "Optional attribute to read instead of text content.", + }, + "index": { + "type": "integer", + "description": "Zero-based index when the selector matches multiple nodes.", + }, + }, + "required": ["url", "select"], + }, + }, + "function": {"type": "scrape"}, + }, + { + "spec": { + "name": "script", + "description": "Execute a Home Assistant script sequence or script entity.", + "parameters": { + "type": "object", + "properties": { + "sequence": { + "type": "array", + "items": {"type": "object"}, + "description": "Home Assistant script steps to run.", + }, + "entity_id": { + "type": "string", + "description": "Existing script entity to run if no sequence is provided.", + }, + }, + }, + }, + "function": {"type": "script"}, + }, + { + "spec": { + "name": "template", + "description": "Render a Home Assistant template with the provided arguments.", + "parameters": { + "type": "object", + "properties": { + "template": { + "type": "string", + "description": "Template string to render.", + }, + "parse_json": { + "type": "boolean", + "description": "Parse the rendered template as JSON when true.", + }, + }, + "required": ["template"], + }, + }, + "function": {"type": "template"}, + }, + { + "spec": { + "name": "composite", + "description": "Run multiple tools sequentially, sharing variables between them.", + "parameters": { + "type": "object", + "properties": { + "sequence": { + "type": "array", + "items": {"type": "object"}, + "description": "List of tool invocations to execute in order.", + }, + }, + "required": ["sequence"], + }, + }, + "function": {"type": "composite"}, + }, ] DEFAULT_FUNCTIONS_RAW = "" DEFAULT_MAX_TOOL_CALLS = 4 diff --git a/custom_components/extended_openai_conversation/conversation.py b/custom_components/extended_openai_conversation/conversation.py index ed275626..490c8a2b 100644 --- a/custom_components/extended_openai_conversation/conversation.py +++ b/custom_components/extended_openai_conversation/conversation.py @@ -7,6 +7,7 @@ import logging from typing import Any, Literal, Optional +from homeassistant.components import conversation as conv from homeassistant.components.conversation import ( AssistantContent, ChatLog, @@ -16,14 +17,14 @@ from homeassistant.helpers import intent from homeassistant.const import CONF_API_KEY from homeassistant.helpers.llm import ToolInput +from openai._exceptions import OpenAIError # Try to import the real ConversationResult; otherwise provide a compatible shim. try: from homeassistant.components.conversation.agent import ConversationResult # type: ignore[attr-defined] except Exception: try: - from homeassistant.components.conversation import agent as _agent_mod # type: ignore[attr-defined] - ConversationResult = _agent_mod.ConversationResult # type: ignore[assignment] + ConversationResult = conv.agent.ConversationResult # type: ignore[assignment] except Exception: class ConversationResult: # type: ignore[misc] @@ -85,6 +86,7 @@ def __init__(self, hass, entry) -> None: self.hass = hass self.entry = entry self._logged_minimal_effort = False + self._preferred_web_search_type: Optional[str] = None @property def supported_languages(self) -> list[str] | Literal["*"]: @@ -105,6 +107,9 @@ async def _async_handle_message( self, user_input: Any, chat_log: ChatLog ) -> ConversationResult: """Handle a message from Assist.""" + # ConversationEntity contract documented in + # homeassistant.components.conversation (2024.12): _async_handle_message + # receives (ConversationInput, ChatLog) and returns ConversationResult. # Lazy import avoids SDK import at module load from .openai_support import build_async_client @@ -284,12 +289,39 @@ async def _run_responses_flow( payload["reasoning"] = {"effort": effort} tools = orchestrator.conversation_tools_for_responses( - self.hass, model=model + self.hass, + model=model, + tool_type_override=self._preferred_web_search_type, ) if tools: payload["tools"] = tools - result = await client.responses.create(**payload) + # Responses API expects tool continuations via function_call_output entries: + # https://platform.openai.com/docs/api-reference/responses/create + try: + result = await client.responses.create(**payload) + except OpenAIError as err: + fallback_type = orchestrator.web_search_fallback_type + current_type = orchestrator.web_search_tool_type + if ( + current_type + and fallback_type + and _should_retry_web_search(current_type, str(err)) + ): + # Some tenants still expose 'web_search_preview'; retry gracefully. + _LOGGER.debug( + "Web search tool type '%s' rejected; retrying with '%s'.", + current_type, + fallback_type, + ) + payload["tools"] = self._swap_web_search_tool_type( + payload.get("tools", []), current_type, fallback_type + ) + orchestrator.update_web_search_tool_type(fallback_type) + self._preferred_web_search_type = fallback_type + result = await client.responses.create(**payload) + else: + raise return await self._handle_responses_tool_calls( client=client, model=model, @@ -399,9 +431,16 @@ async def _handle_responses_tool_calls( ) return result + prev_id = getattr(result, "id", None) + call_ids = [output["call_id"] for output in outputs] + _LOGGER.debug( + "Posting function_call_output for call_id(s) %s with previous_response_id=%s.", + call_ids, + prev_id, + ) result = await client.responses.create( model=model, - previous_response_id=getattr(result, "id", None), + previous_response_id=prev_id, input=outputs, ) _LOGGER.debug( @@ -433,8 +472,9 @@ async def _run_chat_flow( max_tokens = int(options.get(CONF_MAX_TOKENS) or DEFAULT_MAX_TOKENS) if max_tokens > 0: - key = "max_completion_tokens" if caps.is_reasoning else "max_tokens" - kwargs[key] = max_tokens + # OpenAI Chat Completions accepts 'max_tokens' for budget control: + # https://platform.openai.com/docs/api-reference/chat/create + kwargs["max_tokens"] = max_tokens if caps.accepts_temperature: if (t := options.get(CONF_TEMPERATURE)) is not None: @@ -626,6 +666,24 @@ def _log_final_assistant_message( AssistantContent(agent_id=agent_id, content=text or "") ) + @staticmethod + def _swap_web_search_tool_type( + tools: list[dict[str, Any]], current_type: str, new_type: str + ) -> list[dict[str, Any]]: + swapped: list[dict[str, Any]] = [] + for tool in tools: + if tool.get("type") == current_type: + updated = dict(tool) + updated["type"] = new_type + swapped.append(updated) + else: + swapped.append(tool) + _LOGGER.debug( + "Registered hosted web search tool using type '%s'.", + new_type, + ) + return swapped + def _model_supports_minimal_reasoning(model: Optional[str]) -> bool: """Return True if the target model advertises 'minimal' reasoning effort. @@ -639,6 +697,12 @@ def _model_supports_minimal_reasoning(model: Optional[str]) -> bool: return name.startswith("gpt-5") +def _should_retry_web_search(current_type: str, error_text: str) -> bool: + """Detect tool-type errors so we can retry with the preview variant.""" + lowered = error_text.lower() + return current_type in lowered and "web_search" in lowered + + def _ok(*, text: str, language: Optional[str], conversation_id: Optional[str], cont: bool) -> ConversationResult: response = intent.IntentResponse(language=language) response.async_set_speech(text or "") diff --git a/custom_components/extended_openai_conversation/manifest.json b/custom_components/extended_openai_conversation/manifest.json index fe44148d..781c8fb1 100644 --- a/custom_components/extended_openai_conversation/manifest.json +++ b/custom_components/extended_openai_conversation/manifest.json @@ -8,7 +8,7 @@ "iot_class": "cloud_polling", "integration_type": "service", "dependencies": ["conversation"], - "requirements": ["openai>=1.0.0,<2.0.0", "beautifulsoup4>=4.12.0"], + "requirements": ["openai>=1.0.0,<2.0.0", "beautifulsoup4>=4.12"], "version": "1.4.1", "loggers": ["custom_components.extended_openai_conversation"], "platforms": ["conversation"] diff --git a/custom_components/extended_openai_conversation/tools_builtin.py b/custom_components/extended_openai_conversation/tools_builtin.py index 562e5e10..94aaafa3 100644 --- a/custom_components/extended_openai_conversation/tools_builtin.py +++ b/custom_components/extended_openai_conversation/tools_builtin.py @@ -68,6 +68,11 @@ HISTORY_SUMMARY_LIMIT = 10 +# REST helper mirrors https://www.home-assistant.io/integrations/rest/ defaults. +REST_ALLOWED_METHODS = {"GET", "POST", "PUT", "PATCH"} +REST_BODY_MAX_CHARS = 8192 +SCRAPE_SAFE_MAX_INDEX = 50 + def _lazy_import_bs4(): try: @@ -493,6 +498,14 @@ def __init__(self) -> None: """initialize script function""" super().__init__(SCRIPT_ENTITY_SCHEMA) + def to_arguments(self, arguments): + if "sequence" in arguments: + return super().to_arguments(arguments) + entity_id = arguments.get("entity_id") + if isinstance(entity_id, str) and entity_id.startswith("script."): + return {"type": arguments["type"], "entity_id": entity_id} + raise InvalidFunction("script") + async def execute( self, hass: HomeAssistant, @@ -501,19 +514,39 @@ async def execute( user_input: conversation.ConversationInput, exposed_entities, ): - script = Script( - hass, - function["sequence"], - "extended_openai_conversation", - DOMAIN, - running_description="[extended_openai_conversation] function", - logger=_LOGGER, - ) + sequence = function.get("sequence") or arguments.get("sequence") + if sequence: + if not isinstance(sequence, list): + raise HomeAssistantError("Script sequence must be a list of steps.") + script = Script( + hass, + sequence, + "extended_openai_conversation", + DOMAIN, + running_description="[extended_openai_conversation] function", + logger=_LOGGER, + ) + result = await script.async_run( + run_variables=arguments, context=user_input.context + ) + return result.variables.get("_function_result", "Success") + + entity_id = arguments.get("entity_id") + if isinstance(entity_id, str) and entity_id.startswith("script."): + domain, service = entity_id.split(".", 1) + # Execute script entity as documented in + # https://www.home-assistant.io/integrations/script/. + await hass.services.async_call( + domain, + service, + {"entity_id": entity_id}, + context=user_input.context, + ) + return "Success" - result = await script.async_run( - run_variables=arguments, context=user_input.context + raise HomeAssistantError( + "Script tool requires either a 'sequence' or script 'entity_id'." ) - return result.variables.get("_function_result", "Success") class TemplateFunctionExecutor(FunctionExecutor): @@ -536,9 +569,19 @@ async def execute( user_input: conversation.ConversationInput, exposed_entities, ): - return function["value_template"].async_render( + value_template: Template | None = function.get("value_template") + if value_template is None: + template_str = arguments.get("template") + if not isinstance(template_str, str) or not template_str.strip(): + raise HomeAssistantError("Template tool requires a 'template' string.") + value_template = Template(template_str, hass) + + parse_result = function.get("parse_result", arguments.get("parse_json", False)) + # Template helper mirrors https://www.home-assistant.io/docs/configuration/templating/ + # behaviour and renders with provided arguments. + return value_template.async_render( arguments, - parse_result=function.get("parse_result", False), + parse_result=bool(parse_result), ) @@ -562,7 +605,50 @@ async def execute( user_input: conversation.ConversationInput, exposed_entities, ): - config = function + config = dict(function) + + request_url = arguments.get("url") or arguments.get(CONF_RESOURCE) + if request_url: + if not isinstance(request_url, str) or not request_url.lower().startswith( + ("http://", "https://") + ): + raise HomeAssistantError("REST tool requires an http(s) URL.") + config[CONF_RESOURCE] = request_url + if CONF_RESOURCE not in config: + raise HomeAssistantError("REST tool requires a 'url' argument.") + + method = str( + arguments.get("method", config.get(CONF_METHOD, "GET")) + ).upper() + if method not in REST_ALLOWED_METHODS: + raise HomeAssistantError( + f"REST method '{method}' is not allowed; " + f"allowed methods: {', '.join(sorted(REST_ALLOWED_METHODS))}" + ) + config[CONF_METHOD] = method + + headers = arguments.get("headers") + if headers is not None: + if not isinstance(headers, dict): + raise HomeAssistantError("REST headers must be an object of strings.") + safe_headers: dict[str, str] = {} + for key, value in headers.items(): + safe_headers[str(key)[:64]] = str(value)[:256] + config[rest.const.CONF_HEADERS] = safe_headers + + if (payload := arguments.get("payload")) is not None: + payload_str = str(payload) + if len(payload_str) > REST_BODY_MAX_CHARS: + raise HomeAssistantError("REST payload exceeds 8KB limit.") + config[CONF_PAYLOAD] = payload_str + + config.setdefault(CONF_TIMEOUT, min(15, config.get(CONF_TIMEOUT, 10))) + config.setdefault(CONF_VERIFY_SSL, True) + + # REST helper mirrors https://www.home-assistant.io/integrations/rest/ + # behaviour: build data coordinator and read text/JSON. + # Scrape helper aligns with https://www.home-assistant.io/integrations/scrape/ + # by delegating to the shared REST data coordinator. rest_data = _get_rest_data(hass, config, arguments) await rest_data.async_update() @@ -598,7 +684,55 @@ async def execute( exposed_entities, ): _lazy_import_bs4() - config = function + config = dict(function) + + request_url = arguments.get("url") or arguments.get(CONF_RESOURCE) + if request_url: + if not isinstance(request_url, str) or not request_url.lower().startswith( + ("http://", "https://") + ): + raise HomeAssistantError("Scrape tool requires an http(s) URL.") + config[CONF_RESOURCE] = request_url + if CONF_RESOURCE not in config: + raise HomeAssistantError("Scrape tool requires a 'url' argument.") + + sensor_configs = [dict(sensor) for sensor in config.get("sensor", [])] + def _coerce_index(value: Any) -> int: + if value in (None, ""): + return 0 + try: + parsed = int(value) + except (TypeError, ValueError) as exc: + raise HomeAssistantError("Scrape index must be an integer.") from exc + return max(0, min(parsed, SCRAPE_SAFE_MAX_INDEX)) + + if not sensor_configs: + select = arguments.get("select") + if not isinstance(select, str) or not select.strip(): + raise HomeAssistantError("Scrape tool requires a 'select' CSS selector.") + sensor_config: dict[str, Any] = { + scrape.const.CONF_SELECT: select.strip(), + scrape.const.CONF_INDEX: _coerce_index(arguments.get("index")), + } + if attribute := arguments.get("attribute"): + sensor_config[CONF_ATTRIBUTE] = str(attribute) + sensor_configs = [sensor_config] + else: + select = arguments.get("select") + index = arguments.get("index") + attribute = arguments.get("attribute") + target_sensor = sensor_configs[0] + if select: + target_sensor[scrape.const.CONF_SELECT] = str(select) + if index is not None: + target_sensor[scrape.const.CONF_INDEX] = _coerce_index(index) + if attribute is not None: + if attribute == "": + target_sensor.pop(CONF_ATTRIBUTE, None) + else: + target_sensor[CONF_ATTRIBUTE] = str(attribute) + config["sensor"] = sensor_configs + rest_data = _get_rest_data(hass, config, arguments) coordinator = scrape.coordinator.ScrapeCoordinator( hass, @@ -702,7 +836,11 @@ async def execute( exposed_entities, ): config = function - sequence = config["sequence"] + sequence = config.get("sequence") or arguments.get("sequence") + if not isinstance(sequence, list) or not sequence: + raise HomeAssistantError( + "Composite tool requires a non-empty 'sequence' list." + ) for executor_config in sequence: function_executor = get_function_executor(executor_config["type"]) diff --git a/custom_components/extended_openai_conversation/tools_orchestrator.py b/custom_components/extended_openai_conversation/tools_orchestrator.py index aad8733d..3fb0f625 100644 --- a/custom_components/extended_openai_conversation/tools_orchestrator.py +++ b/custom_components/extended_openai_conversation/tools_orchestrator.py @@ -78,10 +78,14 @@ def __init__( self._chain_depth = 0 self._started = time.monotonic() self._exposed_entities: list[dict[str, Any]] | None = None + self._exposed_entities_expires: float | None = None self._tool_specs: list[dict[str, Any]] = [] self._runtime_map: dict[str, dict[str, Any]] = {} + self._web_search_tool_type: str | None = None + self._web_search_fallback_type: str | None = None + self._load_builtin_functions(options.get(CONF_FUNCTIONS, DEFAULT_FUNCTIONS)) self._memory_enabled = options.get(CONF_MEMORY_ENABLED, DEFAULT_MEMORY_ENABLED) self._memory_config = ( @@ -143,16 +147,28 @@ def conversation_tools_for_chat(self) -> list[dict[str, Any]]: ] def conversation_tools_for_responses( - self, hass: HomeAssistant, model: str | None + self, + hass: HomeAssistant, + *, + model: str | None, + tool_type_override: str | None = None, ) -> list[dict[str, Any]]: tools = [ {"type": "function", "function": dict(spec)} for spec in self._tool_specs ] - if web_search := build_responses_web_search_tool( - hass, self.options, model=model + self._web_search_tool_type = None + self._web_search_fallback_type = None + if tool_spec := build_responses_web_search_tool( + hass, + self.options, + model=model, + tool_type_override=tool_type_override, ): - tools.append(web_search) + payload, fallback_type = tool_spec + self._web_search_tool_type = payload.get("type") + self._web_search_fallback_type = fallback_type + tools.append(payload) return tools def configure_chat_web_search(self, model: str | None) -> None: @@ -166,6 +182,9 @@ def reset(self) -> None: self._chain_depth = 0 self._started = time.monotonic() self._exposed_entities = None + self._exposed_entities_expires = None + self._web_search_tool_type = None + self._web_search_fallback_type = None @property def max_chain_depth(self) -> int: @@ -189,7 +208,12 @@ def _ensure_time_budget(self) -> None: raise ToolExecutionError("Tool chain time budget exceeded") def _get_exposed_entities(self) -> list[dict[str, Any]]: - if self._exposed_entities is not None: + now = time.monotonic() + if ( + self._exposed_entities is not None + and self._exposed_entities_expires is not None + and now < self._exposed_entities_expires + ): return self._exposed_entities exposed: list[dict[str, Any]] = [] @@ -205,6 +229,8 @@ def _get_exposed_entities(self) -> list[dict[str, Any]]: } ) self._exposed_entities = exposed + # Expose settings may change from the UI at runtime; refresh every 5 minutes. + self._exposed_entities_expires = now + 300 return exposed def _stringify_result(self, result: Any) -> str: @@ -376,3 +402,17 @@ def _record_tool_result( ) except Exception as err: # pragma: no cover - defensive guard LOGGER.debug("Unable to append tool result to chat log: %s", err) + + @property + def web_search_tool_type(self) -> str | None: + return self._web_search_tool_type + + @property + def web_search_fallback_type(self) -> str | None: + return self._web_search_fallback_type + + def update_web_search_tool_type(self, new_type: str | None) -> None: + if new_type: + self._web_search_tool_type = new_type + # Only allow a single fallback step to avoid loops. + self._web_search_fallback_type = None diff --git a/custom_components/extended_openai_conversation/tools_web_search.py b/custom_components/extended_openai_conversation/tools_web_search.py index 524a5672..83382adf 100644 --- a/custom_components/extended_openai_conversation/tools_web_search.py +++ b/custom_components/extended_openai_conversation/tools_web_search.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import Any, Dict +from typing import Any, Dict, NamedTuple from homeassistant.core import HomeAssistant @@ -18,6 +18,12 @@ LOGGER = logging.getLogger(__name__) + +class WebSearchToolSpec(NamedTuple): + payload: Dict[str, Any] + fallback_type: str | None + + _CONTEXT_ALIASES = { "small": "low", "low": "low", @@ -81,8 +87,12 @@ def _model_supports_hosted_search(model: str | None) -> bool: def build_responses_web_search_tool( - hass: HomeAssistant, options: dict[str, Any], *, model: str | None -) -> dict[str, Any] | None: + hass: HomeAssistant, + options: dict[str, Any], + *, + model: str | None, + tool_type_override: str | None = None, +) -> WebSearchToolSpec | None: """Return Responses API tool payload for hosted web search if enabled.""" if not options.get(CONF_ENABLE_WEB_SEARCH, DEFAULT_ENABLE_WEB_SEARCH): @@ -97,8 +107,13 @@ def build_responses_web_search_tool( context_size = _normalize_context_size( options.get(CONF_WEB_SEARCH_CONTEXT_SIZE, DEFAULT_WEB_SEARCH_CONTEXT_SIZE) ) - tool: dict[str, Any] = { - "type": "web_search", + tool_type = tool_type_override or "web_search" + # OpenAI may expose the legacy 'web_search_preview' type for older tenants; + # prefer 'web_search' and fall back if the API rejects it (see + # https://platform.openai.com/docs/assistants/tools/web-search). + fallback_type = None if tool_type != "web_search" else "web_search_preview" + tool: Dict[str, Any] = { + "type": tool_type, "search_context_size": context_size, } @@ -107,11 +122,11 @@ def build_responses_web_search_tool( tool["user_location"] = location LOGGER.debug( - "Web search tool enabled for model '%s' with context '%s'", + "Hosted web search enabled for model=%s using tool type '%s'.", model or "", - tool["search_context_size"], + tool_type, ) - return tool + return WebSearchToolSpec(tool, fallback_type) def configure_chat_completion_web_search( From a780e2f392f8fb4bc5d9769a72ea72fa152c7d8c Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 29 Oct 2025 16:36:56 -0500 Subject: [PATCH 6/9] =?UTF-8?q?chore:=20prepare=20beta=20=E2=80=94=20final?= =?UTF-8?q?ize=20toolbox=20arg=20normalization=20+=20exposure=20by=20area/?= =?UTF-8?q?device;=20strings=20&=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extended_openai_conversation/const.py | 17 +- .../conversation.py | 6 +- .../exceptions.py | 17 +- .../extended_openai_conversation/strings.json | 6 + .../tools_builtin.py | 228 ++++++++++++++++-- .../tools_orchestrator.py | 3 + .../translations/en.json | 6 + 7 files changed, 251 insertions(+), 32 deletions(-) diff --git a/custom_components/extended_openai_conversation/const.py b/custom_components/extended_openai_conversation/const.py index c16618de..afc80eb2 100644 --- a/custom_components/extended_openai_conversation/const.py +++ b/custom_components/extended_openai_conversation/const.py @@ -171,17 +171,26 @@ }, "method": { "type": "string", - "enum": ["GET", "POST", "PUT", "PATCH"], - "description": "Optional HTTP method (default GET).", + "enum": ["GET", "POST"], + "description": "HTTP method (default GET).", }, "headers": { "type": "object", "additionalProperties": {"type": "string"}, "description": "Optional request headers.", }, - "payload": { + "params": { + "type": "object", + "additionalProperties": {"type": "string"}, + "description": "Optional query parameters.", + }, + "timeout": { + "type": "integer", + "description": "Optional timeout in seconds (default 10).", + }, + "body": { "type": "string", - "description": "Optional request body for write methods.", + "description": "Optional request body for POST.", }, }, "required": ["url"], diff --git a/custom_components/extended_openai_conversation/conversation.py b/custom_components/extended_openai_conversation/conversation.py index 490c8a2b..c37071aa 100644 --- a/custom_components/extended_openai_conversation/conversation.py +++ b/custom_components/extended_openai_conversation/conversation.py @@ -21,10 +21,12 @@ # Try to import the real ConversationResult; otherwise provide a compatible shim. try: - from homeassistant.components.conversation.agent import ConversationResult # type: ignore[attr-defined] + # Prefer modern path (HA 2024.8+): models.ConversationResult + from homeassistant.components.conversation.models import ConversationResult # type: ignore[attr-defined] except Exception: try: - ConversationResult = conv.agent.ConversationResult # type: ignore[assignment] + # Legacy fallback used by older cores + from homeassistant.components.conversation.agent import ConversationResult # type: ignore[attr-defined] except Exception: class ConversationResult: # type: ignore[misc] diff --git a/custom_components/extended_openai_conversation/exceptions.py b/custom_components/extended_openai_conversation/exceptions.py index 8acf551c..e00cb410 100644 --- a/custom_components/extended_openai_conversation/exceptions.py +++ b/custom_components/extended_openai_conversation/exceptions.py @@ -5,9 +5,9 @@ class EntityNotFound(HomeAssistantError): """When referenced entity not found.""" - def __init__(self, entity_id: str) -> None: + def __init__(self, entity_id) -> None: """Initialize error.""" - super().__init__(self, f"entity {entity_id} not found") + super().__init__(f"entity {entity_id} not found") self.entity_id = entity_id def __str__(self) -> str: @@ -18,9 +18,9 @@ def __str__(self) -> str: class EntityNotExposed(HomeAssistantError): """When referenced entity not exposed.""" - def __init__(self, entity_id: str) -> None: + def __init__(self, entity_id) -> None: """Initialize error.""" - super().__init__(self, f"entity {entity_id} not exposed") + super().__init__(f"entity {entity_id} not exposed") self.entity_id = entity_id def __str__(self) -> str: @@ -34,7 +34,6 @@ class CallServiceError(HomeAssistantError): def __init__(self, domain: str, service: str, data: object) -> None: """Initialize error.""" super().__init__( - self, f"unable to call service {domain}.{service} with data {data}. One of 'entity_id', 'area_id', or 'device_id' is required", ) self.domain = domain @@ -51,7 +50,7 @@ class FunctionNotFound(HomeAssistantError): def __init__(self, function: str) -> None: """Initialize error.""" - super().__init__(self, f"function '{function}' does not exist") + super().__init__(f"function '{function}' does not exist") self.function = function def __str__(self) -> str: @@ -64,7 +63,7 @@ class NativeNotFound(HomeAssistantError): def __init__(self, name: str) -> None: """Initialize error.""" - super().__init__(self, f"native function '{name}' does not exist") + super().__init__(f"native function '{name}' does not exist") self.name = name def __str__(self) -> str: @@ -78,7 +77,6 @@ class FunctionLoadFailed(HomeAssistantError): def __init__(self) -> None: """Initialize error.""" super().__init__( - self, "failed to load functions. Verify functions are valid in a yaml format", ) @@ -93,7 +91,6 @@ class ParseArgumentsFailed(HomeAssistantError): def __init__(self, arguments: str) -> None: """Initialize error.""" super().__init__( - self, f"failed to parse arguments `{arguments}`. Increase maximum token to avoid the issue.", ) self.arguments = arguments @@ -109,7 +106,6 @@ class TokenLengthExceededError(HomeAssistantError): def __init__(self, token: int) -> None: """Initialize error.""" super().__init__( - self, f"token length(`{token}`) exceeded. Increase maximum token to avoid the issue.", ) self.token = token @@ -125,7 +121,6 @@ class InvalidFunction(HomeAssistantError): def __init__(self, function_name: str) -> None: """Initialize error.""" super().__init__( - self, f"failed to validate function `{function_name}`", ) self.function_name = function_name diff --git a/custom_components/extended_openai_conversation/strings.json b/custom_components/extended_openai_conversation/strings.json index 63125971..ffa7a934 100644 --- a/custom_components/extended_openai_conversation/strings.json +++ b/custom_components/extended_openai_conversation/strings.json @@ -54,5 +54,11 @@ } } } + }, + "errors": { + "rest_url_required": "REST tool requires a 'url' (http/https)", + "scrape_requirements": "Scrape tool requires a 'url' and 'select'", + "template_required": "Template tool requires a 'template' or 'value_template'", + "exposure_denied": "Some targets are hidden from Assist: {entities}" } } diff --git a/custom_components/extended_openai_conversation/tools_builtin.py b/custom_components/extended_openai_conversation/tools_builtin.py index 94aaafa3..a6647b86 100644 --- a/custom_components/extended_openai_conversation/tools_builtin.py +++ b/custom_components/extended_openai_conversation/tools_builtin.py @@ -33,8 +33,10 @@ from homeassistant.config import AUTOMATION_CONFIG_PATH from homeassistant.const import ( CONF_ATTRIBUTE, + CONF_HEADERS, CONF_METHOD, CONF_NAME, + CONF_PARAMS, CONF_PAYLOAD, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, @@ -69,7 +71,7 @@ HISTORY_SUMMARY_LIMIT = 10 # REST helper mirrors https://www.home-assistant.io/integrations/rest/ defaults. -REST_ALLOWED_METHODS = {"GET", "POST", "PUT", "PATCH"} +REST_ALLOWED_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE"} REST_BODY_MAX_CHARS = 8192 SCRAPE_SAFE_MAX_INDEX = 50 @@ -299,7 +301,71 @@ async def execute_service_single( raise CallServiceError(domain, service, service_data) if not hass.services.has_service(domain, service): raise ServiceNotFound(domain, service) + + # If explicit entity_id is provided, validate exposure as usual and drop area/device self.validate_entity_ids(hass, entity_id or [], exposed_entities) + if entity_id: + # Avoid bypass via extra selectors + service_data.pop("area_id", None) + service_data.pop("device_id", None) + + # If no entity_id but area_id/device_id is provided, resolve targets and enforce exposure + if not entity_id and (area_id or device_id): + from homeassistant.helpers import area_registry as ar, device_registry as dr, entity_registry as er # lazy import + from homeassistant.helpers.exposed_entities import async_should_expose + + def _as_list(x): + if x is None: + return [] + if isinstance(x, list): + return x + return [x] + + area_ids = [a for a in _as_list(area_id) if a] + device_ids = [d for d in _as_list(device_id) if d] + + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + targets: set[str] = set() + + # Resolve by device_id + if device_ids: + for entry in ent_reg.entities.values(): + if entry.device_id and entry.device_id in device_ids: + targets.add(entry.entity_id) + + # Resolve by area_id (direct entity area or via device area) + if area_ids: + # Map device_id → area_id for quick lookups + device_area = {dev.id: dev.area_id for dev in dev_reg.devices.values()} + for entry in ent_reg.entities.values(): + if entry.area_id and entry.area_id in area_ids: + targets.add(entry.entity_id) + continue + if entry.device_id and device_area.get(entry.device_id) in area_ids: + targets.add(entry.entity_id) + + # Filter to currently loaded entities only (avoid phantom registry entries) + targets = {eid for eid in targets if hass.states.get(eid) is not None} + + hidden = [eid for eid in sorted(targets) if not async_should_expose(hass, "conversation", eid)] + if hidden: + _LOGGER.debug( + "Exposure denied for assistant='conversation', hidden targets=%s", + hidden, + ) + return {"error": f"Some targets are hidden from Assist: {', '.join(hidden)}"} + + if not targets: + return {"error": "No exposed targets found for the provided area/device"} + + if targets: + # Limit the actual call to exposed targets only + service_data = dict(service_data) + service_data["entity_id"] = list(sorted(targets)) + # Avoid passing area/device ids to the call to prevent bypass + service_data.pop("area_id", None) + service_data.pop("device_id", None) try: await hass.services.async_call( @@ -557,10 +623,27 @@ def __init__(self) -> None: { vol.Required("value_template"): cv.template, vol.Optional("parse_result"): bool, + # Accept variables mapping for render context + vol.Optional("vars", default={}): dict, } ) ) + def to_arguments(self, arguments): + """Normalize friendly keys before schema validation. + + Accepts 'template' → maps to 'value_template' and preserves optional 'vars'. + Verified against HA template helper contract (cv.template + Template render). + """ + mapped = dict(arguments) + tmpl = mapped.get("value_template") + if tmpl is None and isinstance(mapped.get("template"), str): + mapped["value_template"] = mapped.pop("template") + # Ensure vars is a dict if provided + if "vars" in mapped and not isinstance(mapped["vars"], dict): + raise vol.Invalid("vars must be a mapping") + return self.data_schema(mapped) + async def execute( self, hass: HomeAssistant, @@ -569,18 +652,27 @@ async def execute( user_input: conversation.ConversationInput, exposed_entities, ): - value_template: Template | None = function.get("value_template") + value_template: Template | None = function.get("value_template") or arguments.get("value_template") if value_template is None: template_str = arguments.get("template") if not isinstance(template_str, str) or not template_str.strip(): - raise HomeAssistantError("Template tool requires a 'template' string.") + raise HomeAssistantError("Template tool requires a 'template' or 'value_template'") value_template = Template(template_str, hass) parse_result = function.get("parse_result", arguments.get("parse_json", False)) - # Template helper mirrors https://www.home-assistant.io/docs/configuration/templating/ - # behaviour and renders with provided arguments. + # Merge provided vars mapping (function-level or call-level) into the context + ctx_vars = {} + if isinstance(function.get("vars"), dict): + ctx_vars.update(function["vars"]) + if isinstance(arguments.get("vars"), dict): + ctx_vars.update(arguments["vars"]) + # Render using HA template environment (async, non-blocking). + # Pass all arguments as context plus optional 'vars'. + merged_context = dict(arguments) + if ctx_vars: + merged_context.update(ctx_vars) return value_template.async_render( - arguments, + merged_context, parse_result=bool(parse_result), ) @@ -597,6 +689,56 @@ def __init__(self) -> None: ) ) + def to_arguments(self, arguments): + """Normalize friendly keys (url, method, headers, params, timeout, body). + + Map to REST data schema (resource, method, headers, params, timeout, payload). + Enforce http/https and request method allowlist including DELETE. + See HA REST integration docs for RESOURCE_SCHEMA fields. + """ + mapped = dict(arguments) + # url → resource + url = mapped.pop("url", mapped.get(CONF_RESOURCE)) + if url is None: + raise vol.Invalid("REST tool requires a 'url' (http/https)") + if not isinstance(url, str) or not url.lower().startswith(("http://", "https://")): + raise vol.Invalid("REST tool requires a 'url' (http/https)") + mapped[CONF_RESOURCE] = url + + # method allowlist (include DELETE) + method = str(mapped.pop("method", mapped.get(CONF_METHOD, "GET"))).upper() + # HA rest.RESOURCE_SCHEMA restricts methods to rest.const.METHODS (POST/GET) + # https://github.com/home-assistant/core/blob/dev/homeassistant/components/rest/schema.py + if method not in {"GET", "POST"}: + raise vol.Invalid("REST method not allowed") + mapped[CONF_METHOD] = method + + # headers + if "headers" in mapped: + if not isinstance(mapped["headers"], dict): + raise vol.Invalid("headers must be an object") + mapped[CONF_HEADERS] = mapped.pop("headers") + + # params + if "params" in mapped: + if not isinstance(mapped["params"], dict): + raise vol.Invalid("params must be an object") + mapped[CONF_PARAMS] = {str(k): str(v) for k, v in mapped["params"].items()} + mapped.pop("params", None) + + # timeout + if "timeout" in mapped: + try: + mapped[CONF_TIMEOUT] = int(mapped.pop("timeout")) + except Exception as exc: + raise vol.Invalid("timeout must be an integer") from exc + + # body → payload (cap also enforced in execute) + if "body" in mapped and CONF_PAYLOAD not in mapped: + mapped[CONF_PAYLOAD] = str(mapped.pop("body")) + + return self.data_schema(mapped) + async def execute( self, hass: HomeAssistant, @@ -607,6 +749,11 @@ async def execute( ): config = dict(function) + # Merge normalized arguments from to_arguments into config + for key in (CONF_RESOURCE, CONF_METHOD, CONF_HEADERS, CONF_PARAMS, CONF_TIMEOUT, CONF_VERIFY_SSL, CONF_PAYLOAD): + if key in arguments: + config[key] = arguments[key] + request_url = arguments.get("url") or arguments.get(CONF_RESOURCE) if request_url: if not isinstance(request_url, str) or not request_url.lower().startswith( @@ -617,9 +764,7 @@ async def execute( if CONF_RESOURCE not in config: raise HomeAssistantError("REST tool requires a 'url' argument.") - method = str( - arguments.get("method", config.get(CONF_METHOD, "GET")) - ).upper() + method = str(arguments.get("method", config.get(CONF_METHOD, "GET"))).upper() if method not in REST_ALLOWED_METHODS: raise HomeAssistantError( f"REST method '{method}' is not allowed; " @@ -627,14 +772,20 @@ async def execute( ) config[CONF_METHOD] = method - headers = arguments.get("headers") + # Prefer normalized CONF_HEADERS set by to_arguments + headers = arguments.get(CONF_HEADERS) or arguments.get("headers") if headers is not None: if not isinstance(headers, dict): raise HomeAssistantError("REST headers must be an object of strings.") - safe_headers: dict[str, str] = {} - for key, value in headers.items(): - safe_headers[str(key)[:64]] = str(value)[:256] - config[rest.const.CONF_HEADERS] = safe_headers + safe_headers: dict[str, str] = {str(k)[:64]: str(v)[:256] for k, v in headers.items()} + config[CONF_HEADERS] = safe_headers + + # params + if (CONF_PARAMS in arguments) or ("params" in arguments): + params = arguments.get(CONF_PARAMS, arguments.get("params")) + if not isinstance(params, dict): + raise HomeAssistantError("REST params must be an object of strings.") + config[CONF_PARAMS] = {str(k): str(v) for k, v in params.items()} if (payload := arguments.get("payload")) is not None: payload_str = str(payload) @@ -642,7 +793,14 @@ async def execute( raise HomeAssistantError("REST payload exceeds 8KB limit.") config[CONF_PAYLOAD] = payload_str - config.setdefault(CONF_TIMEOUT, min(15, config.get(CONF_TIMEOUT, 10))) + # timeout + if (CONF_TIMEOUT in arguments) or ("timeout" in arguments): + try: + config[CONF_TIMEOUT] = int(arguments.get(CONF_TIMEOUT, arguments.get("timeout"))) + except Exception as exc: + raise HomeAssistantError("timeout must be an integer") from exc + else: + config.setdefault(CONF_TIMEOUT, min(15, config.get(CONF_TIMEOUT, 10))) config.setdefault(CONF_VERIFY_SSL, True) # REST helper mirrors https://www.home-assistant.io/integrations/rest/ @@ -675,6 +833,43 @@ def __init__(self) -> None: ) ) + def to_arguments(self, arguments): + """Normalize friendly keys (url, select, attr, index) to COMBINED_SCHEMA. + + Friendly keys are mapped before validation so LLM calls don't fail early. + Verified against HA scrape integration (selector/attribute/index semantics). + """ + mapped = dict(arguments) + # url → resource + url = mapped.pop("url", mapped.get(CONF_RESOURCE)) + if url is None: + raise vol.Invalid("Scrape tool requires a 'url'") + if not isinstance(url, str) or not url.lower().startswith(("http://", "https://")): + raise vol.Invalid("Scrape tool requires a 'url' (http/https)") + mapped[CONF_RESOURCE] = url + + # Build sensor list from friendly keys if absent + if "sensor" not in mapped: + select = mapped.pop("select", None) + if not isinstance(select, str) or not select.strip(): + raise vol.Invalid("Scrape tool requires a 'select' CSS selector") + idx = mapped.pop("index", 0) + try: + idx = int(idx) + except Exception as exc: + raise vol.Invalid("index must be an integer") from exc + idx = max(0, min(idx, SCRAPE_SAFE_MAX_INDEX)) + sensor_cfg = { + scrape.const.CONF_SELECT: select.strip(), + scrape.const.CONF_INDEX: idx, + } + attr = mapped.pop("attr", mapped.pop("attribute", None)) + if attr is not None: + sensor_cfg[CONF_ATTRIBUTE] = str(attr) + mapped["sensor"] = [sensor_cfg] + + return self.data_schema(mapped) + async def execute( self, hass: HomeAssistant, @@ -697,6 +892,9 @@ async def execute( raise HomeAssistantError("Scrape tool requires a 'url' argument.") sensor_configs = [dict(sensor) for sensor in config.get("sensor", [])] + # Prefer normalized sensors from to_arguments if present + if not sensor_configs and isinstance(arguments.get("sensor"), list): + sensor_configs = [dict(s) for s in arguments["sensor"]] def _coerce_index(value: Any) -> int: if value in (None, ""): return 0 diff --git a/custom_components/extended_openai_conversation/tools_orchestrator.py b/custom_components/extended_openai_conversation/tools_orchestrator.py index 3fb0f625..a1e046d4 100644 --- a/custom_components/extended_openai_conversation/tools_orchestrator.py +++ b/custom_components/extended_openai_conversation/tools_orchestrator.py @@ -185,6 +185,9 @@ def reset(self) -> None: self._exposed_entities_expires = None self._web_search_tool_type = None self._web_search_fallback_type = None + # Note: Options save/reconfigure triggers a reload of the entity which + # constructs a new orchestrator. This reset (and the reload) ensure the + # exposure cache reflects current UI settings immediately. @property def max_chain_depth(self) -> int: diff --git a/custom_components/extended_openai_conversation/translations/en.json b/custom_components/extended_openai_conversation/translations/en.json index 3273237e..5b674c0e 100644 --- a/custom_components/extended_openai_conversation/translations/en.json +++ b/custom_components/extended_openai_conversation/translations/en.json @@ -56,5 +56,11 @@ } } } + }, + "errors": { + "rest_url_required": "REST tool requires a 'url' (http/https)", + "scrape_requirements": "Scrape tool requires a 'url' and 'select'", + "template_required": "Template tool requires a 'template' or 'value_template'", + "exposure_denied": "Some targets are hidden from Assist: {entities}" } } From bce84bdac3c27af75332bb33d269dae00a72d9f7 Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 29 Oct 2025 17:59:15 -0500 Subject: [PATCH 7/9] fix(tools): finalize REST/scrape/template arg normalization; enforce area/device exposure; strings & polish --- .../extended_openai_conversation/tools_builtin.py | 4 ++-- .../extended_openai_conversation/tools_orchestrator.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/extended_openai_conversation/tools_builtin.py b/custom_components/extended_openai_conversation/tools_builtin.py index a6647b86..b6793964 100644 --- a/custom_components/extended_openai_conversation/tools_builtin.py +++ b/custom_components/extended_openai_conversation/tools_builtin.py @@ -212,7 +212,7 @@ def validate_entity_ids(self, hass: HomeAssistant, entity_ids, exposed_entities) missing = set(entity_ids) - exposed_entity_ids if missing: # Exposure follows Assist's 'conversation' assistant toggle (see - # homeassistant.helpers.exposed_entities.async_should_expose). + # homeassistant.components.homeassistant.exposed_entities.async_should_expose). _LOGGER.debug( "Exposure denied for assistant='conversation', entities=%s", sorted(missing), @@ -312,7 +312,7 @@ async def execute_service_single( # If no entity_id but area_id/device_id is provided, resolve targets and enforce exposure if not entity_id and (area_id or device_id): from homeassistant.helpers import area_registry as ar, device_registry as dr, entity_registry as er # lazy import - from homeassistant.helpers.exposed_entities import async_should_expose + from homeassistant.components.homeassistant.exposed_entities import async_should_expose def _as_list(x): if x is None: diff --git a/custom_components/extended_openai_conversation/tools_orchestrator.py b/custom_components/extended_openai_conversation/tools_orchestrator.py index a1e046d4..aeb2f2da 100644 --- a/custom_components/extended_openai_conversation/tools_orchestrator.py +++ b/custom_components/extended_openai_conversation/tools_orchestrator.py @@ -10,7 +10,7 @@ from typing import Any from homeassistant.components.conversation.chat_log import ChatLog, ToolResultContent -from homeassistant.helpers.exposed_entities import async_should_expose +from homeassistant.components.homeassistant.exposed_entities import async_should_expose from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -222,7 +222,7 @@ def _get_exposed_entities(self) -> list[dict[str, Any]]: exposed: list[dict[str, Any]] = [] for state in self.hass.states.async_all(): # Assist exposure uses the 'conversation' assistant id; see - # homeassistant.helpers.exposed_entities.async_should_expose. + # homeassistant.components.homeassistant.exposed_entities.async_should_expose. if async_should_expose(self.hass, "conversation", state.entity_id): exposed.append( { From 588b8b2191c0fd35d18974e7a60d5fe233713757 Mon Sep 17 00:00:00 2001 From: Da3dalusCode <26097672+Da3dalusCode@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:57:55 -0500 Subject: [PATCH 8/9] Update manifest.json --- custom_components/extended_openai_conversation/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/extended_openai_conversation/manifest.json b/custom_components/extended_openai_conversation/manifest.json index 781c8fb1..ac7c637c 100644 --- a/custom_components/extended_openai_conversation/manifest.json +++ b/custom_components/extended_openai_conversation/manifest.json @@ -9,7 +9,7 @@ "integration_type": "service", "dependencies": ["conversation"], "requirements": ["openai>=1.0.0,<2.0.0", "beautifulsoup4>=4.12"], - "version": "1.4.1", + "version": "1.5.4b1", "loggers": ["custom_components.extended_openai_conversation"], "platforms": ["conversation"] } From 043822c450994e79511c17412bfe9d0adbfa7d52 Mon Sep 17 00:00:00 2001 From: Da3dalusCode <26097672+Da3dalusCode@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:58:48 -0500 Subject: [PATCH 9/9] Update manifest.json --- custom_components/extended_openai_conversation/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/extended_openai_conversation/manifest.json b/custom_components/extended_openai_conversation/manifest.json index ac7c637c..7964e0a1 100644 --- a/custom_components/extended_openai_conversation/manifest.json +++ b/custom_components/extended_openai_conversation/manifest.json @@ -9,7 +9,7 @@ "integration_type": "service", "dependencies": ["conversation"], "requirements": ["openai>=1.0.0,<2.0.0", "beautifulsoup4>=4.12"], - "version": "1.5.4b1", + "version": "1.5.5b1", "loggers": ["custom_components.extended_openai_conversation"], "platforms": ["conversation"] }