Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ A maintained fork of **Extended OpenAI Conversation** for **Home Assistant** tha
- No `temperature/top_p` on reasoning endpoints
- **Chat Completions** for non‑reasoning models (sampling allowed)
- **Options gear** (model, strategy, effort, max tokens, temp/top‑p, prompt) via `OptionsFlowWithReload`
- **Toolbox parity** with execute service / automation / history built-ins plus YAML-defined scripts, REST, scrape, template, composite helpers
- **Hosted web search** toggle mirroring the stock OpenAI agent (Responses API + graceful chat fallback)
- **Optional MCP bridge** that surfaces Home Assistant MCP servers as namespaced tools
- **Azure OpenAI** support (Base URL + API version)
- **Async‑safe client** using HA’s shared HTTPX to avoid blocking SSL CA loads

Expand All @@ -34,11 +37,12 @@ A maintained fork of **Extended OpenAI Conversation** for **Home Assistant** tha

## Configure
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`)
- **Reasoning effort**: `minimal` \| `low` \| `medium` \| `high`
- **Max tokens**, **Temperature**, **Top‑p**, **System prompt**
- **Model & Strategy** – choose default model plus routing strategy between Chat vs Responses API.
- **Reasoning effort & token limits** – effort hint for reasoning models, max completion/output tokens, temperature & top‑p (chat only).
- **Hosted Web Search** – enable OpenAI’s web search tool, set context size, optionally include approximate home location metadata.
- **Toolbox limits** – cap total tool calls per user turn.
- **MCP bridge** – opt‑in to surface any configured MCP servers (timeout + payload guardrails).
- **Functions (YAML)** – edit the toolbox definition (defaults include `execute_service`, `add_automation`, `get_history`); append your own `rest`, `scrape`, `script`, `template`, or `composite` functions.

### Recommended (long, smart chats)
- Model: `gpt-5`
Expand All @@ -48,6 +52,12 @@ Open the integration card → **Configure** (gear):
- Max tokens: **800–1200**
> With Responses API, *system text* goes into **`instructions`** and inputs use **`input_text`**.

## Toolbox YAML quick reference
- Default entries provide `execute_service`, `add_automation`, and `get_history` using the historical EOC schema.
- Append additional entries to expose `script`, `template`, `rest`, `scrape`, or `composite` actions.
- Each entry requires a `spec` (tool definition shown to the model) and `function` (executor metadata); the existing [upstream examples](https://github.com/jekalmin/extended_openai_conversation/tree/main/examples/function) remain compatible.
- Invalid YAML is rejected by the options flow and logged with context so setup continues safely.

## Assist usage
- **Settings → Voice Assistants** → set **Conversation agent** = *Extended OpenAI Conversation*
- Use Assist (text/voice) or ESPHome satellites normally.
Expand All @@ -58,6 +68,10 @@ Open the integration card → **Configure** (gear):
- **`intent-failed` with `.as_dict`**: fixed in v1.4.1 (compat shim). :contentReference[oaicite:9]{index=9}
- **Blocking SSL warning (`load_verify_locations`)**: fixed by using HA’s shared HTTPX client in v1.4.1. :contentReference[oaicite:10]{index=10}

## Web search & MCP notes
- Web search is sent only on Responses API routes. For chat-only models we log a notice and the agent replies without search context.
- MCP tools are surfaced when Home Assistant (or custom code) registers MCP servers under `hass.data["mcp_servers"]`; each tool call is sandboxed with timeout and payload limits.

Enable debug:
```yaml
action: logger.set_level
Expand Down
91 changes: 81 additions & 10 deletions custom_components/extended_openai_conversation/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +27,14 @@
CONF_TOP_P,
CONF_MAX_TOKENS,
CONF_REASONING_EFFORT,
CONF_FUNCTIONS_YAML,
CONF_ENABLE_WEB_SEARCH,
CONF_SEARCH_CONTEXT_SIZE,
CONF_INCLUDE_HOME_LOCATION,
CONF_MAX_TOOL_CALLS,
CONF_ENABLE_MCP,
CONF_MCP_TIMEOUT,
CONF_MCP_MAX_PAYLOAD,
DEFAULT_BASE_URL,
DEFAULT_CHAT_MODEL,
DEFAULT_MODEL_STRATEGY,
Expand All @@ -34,7 +43,17 @@
DEFAULT_TOP_P,
DEFAULT_MAX_TOKENS,
DEFAULT_PROMPT,
DEFAULT_ENABLE_WEB_SEARCH,
DEFAULT_SEARCH_CONTEXT_SIZE,
DEFAULT_INCLUDE_HOME_LOCATION,
DEFAULT_MAX_TOOL_CALLS,
DEFAULT_ENABLE_MCP,
DEFAULT_MCP_TIMEOUT,
DEFAULT_MCP_MAX_PAYLOAD,
)
from .tools_builtin import build_default_functions_yaml

DEFAULT_FUNCTIONS_YAML = build_default_functions_yaml()


class ExtendedOpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
Expand Down Expand Up @@ -105,29 +124,81 @@ def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_e
class EOCOptionsFlow(OptionsFlowWithReload):
"""Options flow that auto-reloads on save."""

def _validate_functions_yaml(self, functions_text: str | None) -> str | None:
"""Ensure the provided YAML parses into a list/dict structure."""
if not functions_text:
return ""
try:
data = yaml.safe_load(functions_text) if functions_text.strip() else None
except yaml.YAMLError as err:
raise vol.Invalid(f"Invalid YAML: {err}") from err

if data is None:
return ""
if not isinstance(data, (list, dict)):
raise vol.Invalid("Functions YAML must be a list or mapping.")
return functions_text

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
if user_input is not None:
try:
user_input[CONF_FUNCTIONS_YAML] = self._validate_functions_yaml(
user_input.get(CONF_FUNCTIONS_YAML)
)
except vol.Invalid as err:
errors = {"base": "invalid_functions_yaml"}
return self.async_show_form(
step_id="init",
data_schema=self._build_schema(self.config_entry.options, user_input),
errors=errors,
)

return self.async_create_entry(data=user_input)

# Build the form with suggested current values.
opts = self.config_entry.options

schema = vol.Schema(
return self.async_show_form(
step_id="init",
data_schema=self._build_schema(opts),
)

def _build_schema(
self, opts: dict[str, Any], user_input: dict[str, Any] | None = None
) -> vol.Schema:
"""Build the options schema with defaults."""
data = user_input or opts
return vol.Schema(
{
vol.Optional(CONF_CHAT_MODEL, default=opts.get(CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL)): str,
vol.Optional(CONF_MODEL_STRATEGY, default=opts.get(CONF_MODEL_STRATEGY, DEFAULT_MODEL_STRATEGY)): vol.In(
vol.Optional(CONF_CHAT_MODEL, default=data.get(CONF_CHAT_MODEL, DEFAULT_CHAT_MODEL)): str,
vol.Optional(CONF_MODEL_STRATEGY, default=data.get(CONF_MODEL_STRATEGY, DEFAULT_MODEL_STRATEGY)): vol.In(
["auto", "force_chat_completions", "force_responses_api"]
),
vol.Optional(CONF_USE_RESPONSES_API, default=opts.get(CONF_USE_RESPONSES_API, True)): bool,
vol.Optional(CONF_REASONING_EFFORT, default=opts.get(CONF_REASONING_EFFORT, "medium")): vol.In(
vol.Optional(CONF_USE_RESPONSES_API, default=data.get(CONF_USE_RESPONSES_API, DEFAULT_USE_RESPONSES_API)): bool,
vol.Optional(CONF_REASONING_EFFORT, default=data.get(CONF_REASONING_EFFORT, "medium")): vol.In(
["minimal", "low", "medium", "high"]
),
vol.Optional(CONF_TEMPERATURE, default=opts.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.Coerce(float),
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_TEMPERATURE, default=data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.Coerce(float),
vol.Optional(CONF_TOP_P, default=data.get(CONF_TOP_P, DEFAULT_TOP_P)): vol.Coerce(float),
vol.Optional(CONF_MAX_TOKENS, default=data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)): vol.Coerce(int),
vol.Optional(CONF_ENABLE_WEB_SEARCH, default=data.get(CONF_ENABLE_WEB_SEARCH, DEFAULT_ENABLE_WEB_SEARCH)): bool,
vol.Optional(CONF_SEARCH_CONTEXT_SIZE, default=data.get(CONF_SEARCH_CONTEXT_SIZE, DEFAULT_SEARCH_CONTEXT_SIZE)): vol.All(
vol.Coerce(int), vol.Range(min=1, max=32)
),
vol.Optional(CONF_INCLUDE_HOME_LOCATION, default=data.get(CONF_INCLUDE_HOME_LOCATION, DEFAULT_INCLUDE_HOME_LOCATION)): bool,
vol.Optional(CONF_MAX_TOOL_CALLS, default=data.get(CONF_MAX_TOOL_CALLS, DEFAULT_MAX_TOOL_CALLS)): vol.All(
vol.Coerce(int), vol.Range(min=1, max=16)
),
vol.Optional(CONF_ENABLE_MCP, default=data.get(CONF_ENABLE_MCP, DEFAULT_ENABLE_MCP)): bool,
vol.Optional(CONF_MCP_TIMEOUT, default=data.get(CONF_MCP_TIMEOUT, DEFAULT_MCP_TIMEOUT)): vol.All(
vol.Coerce(int), vol.Range(min=1, max=120)
),
vol.Optional(CONF_MCP_MAX_PAYLOAD, default=data.get(CONF_MCP_MAX_PAYLOAD, DEFAULT_MCP_MAX_PAYLOAD)): vol.All(
vol.Coerce(int), vol.Range(min=1024, max=65536)
),
vol.Optional(CONF_FUNCTIONS_YAML, default=data.get(CONF_FUNCTIONS_YAML, DEFAULT_FUNCTIONS_YAML)): str,
vol.Optional("prompt", default=data.get("prompt", DEFAULT_PROMPT)): str,
}
)
return self.async_show_form(step_id="init", data_schema=schema)
15 changes: 15 additions & 0 deletions custom_components/extended_openai_conversation/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@
# Optional scaffolding (off by default)
CONF_MEMORY_ENABLED = "memory_enabled"
CONF_MEMORY_DEFAULT_NAMESPACE = "memory_default_namespace"
CONF_FUNCTIONS_YAML = "functions"
CONF_ENABLE_WEB_SEARCH = "enable_web_search"
CONF_SEARCH_CONTEXT_SIZE = "search_context_size"
CONF_INCLUDE_HOME_LOCATION = "include_home_location"
CONF_MAX_TOOL_CALLS = "max_tool_calls"
CONF_ENABLE_MCP = "enable_mcp_tools"
CONF_MCP_TIMEOUT = "mcp_timeout"
CONF_MCP_MAX_PAYLOAD = "mcp_max_payload"

# Optional service
SERVICE_QUERY_IMAGE = "query_image"
Expand All @@ -47,3 +55,10 @@
DEFAULT_PROMPT = ""
DEFAULT_MEMORY_ENABLED = False
DEFAULT_MEMORY_DEFAULT_NAMESPACE = "default"
DEFAULT_ENABLE_WEB_SEARCH = False
DEFAULT_SEARCH_CONTEXT_SIZE = 8
DEFAULT_INCLUDE_HOME_LOCATION = False
DEFAULT_MAX_TOOL_CALLS = 4
DEFAULT_ENABLE_MCP = False
DEFAULT_MCP_TIMEOUT = 20
DEFAULT_MCP_MAX_PAYLOAD = 16384
Loading