diff --git a/README.md b/README.md
index a2fa1cf..02b1c4b 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,9 @@
-
+
+
@@ -26,7 +27,7 @@ Meeseeks is an innovative AI assistant built on a multi-agent large language mod
| Completed | In-Progress | Planned | Scoping |
| :-------: | :---------: | :-----: | :-----: |
-| ✅ | 🚧 | 📅 | 🧐 |
+| ✅ | 🚧 | 📅 | 🧐 |
@@ -34,22 +35,33 @@ Meeseeks is an innovative AI assistant built on a multi-agent large language mod
> [!NOTE]
> Visit [**Features - Wiki**](https://github.com/bearlike/Personal-Assistant/wiki/Features) for detailed information on tools and integration capabilities.
+
+
+ Answer questions and interpret sensor information |
+ Control devices and entities |
+
+
+ |
+ |
+
+
+
- (✅) [LangFuse](https://github.com/langfuse/langfuse) integrations to accurate log and monitor chains.
- (✅) Use natural language to interact with integrations and tools.
-- (🚧) Simple REST API interface for 3rd party tools to interface with Meeseeks.
+- (✅) Simple REST API interface for 3rd party tools to interface with Meeseeks.
- (✅) Handles complex user queries by breaking them into actionable steps, executing these steps, and then summarizing on the results.
-- (🚧) Custom [Home Assistant Conversation Integration](https://www.home-assistant.io/integrations/conversation/) to allow voice assistance via [**HA Assist**](https://www.home-assistant.io/voice_control/).
+- (✅) Custom [Home Assistant Conversation Integration](https://www.home-assistant.io/integrations/conversation/) to allow voice assistance via [**HA Assist**](https://www.home-assistant.io/voice_control/).
- (✅) A chat Interface using `streamlit` that shows the action plan, user types, and response from the LLM.
## Extras 👽
Optional feature that users can choose to install to further optimize their experience.
-- (🧐) **`Quality`** Use [CRITIC reflection framework](https://arxiv.org/pdf/2305.11738) to reflect on a response to a task/query using external tools via [`[^]`](https://llamahub.ai/l/agent/llama-index-agent-introspective).
-- (📅) **`Privacy`** Integrate with [microsoft/presidio](https://github.com/microsoft/presidio) for customizable PII de-identification.
+- (📅) **`Quality`** Use [CRITIC reflection framework](https://arxiv.org/pdf/2305.11738) to reflect on a response to a task/query using external tools via [`[^]`](https://llamahub.ai/l/agent/llama-index-agent-introspective).
+- (🚧) **`Privacy`** Integrate with [microsoft/presidio](https://github.com/microsoft/presidio) for customizable PII de-identification.
## Integrations 📦
- (✅) [Home Assistant](https://github.com/home-assistant/core)
- (🚧) Google Calendar
-- (📅) Google Search, Search recent ArXiv papers and summaries, Yahoo Finance, Yelp
+- (🚧) Google Search, Search recent ArXiv papers and summaries, Yahoo Finance, Yelp
- (🧐) Android Debugging Shell
## Installating and Running Meeseeks
diff --git a/docs/screenshot_ha_assist_1.png b/docs/screenshot_ha_assist_1.png
new file mode 100644
index 0000000..c8a01a5
Binary files /dev/null and b/docs/screenshot_ha_assist_1.png differ
diff --git a/docs/screenshot_ha_assist_2.png b/docs/screenshot_ha_assist_2.png
new file mode 100644
index 0000000..b76a86d
Binary files /dev/null and b/docs/screenshot_ha_assist_2.png differ
diff --git a/meeseeks-api/README.md b/meeseeks-api/README.md
index d9a7453..819032d 100644
--- a/meeseeks-api/README.md
+++ b/meeseeks-api/README.md
@@ -1,6 +1,13 @@
-# meeseeks-api
+# Meeseeks API Server
+
+
+
+
+
- REST API Engine wrapped around the meeseeks-core.
- No components are explicitly tested for safety or security. Use with caution in a production environment.
+- For more information, such as installation, please check out the [Wiki](https://github.com/bearlike/Personal-Assistant/wiki).
+
[Link to GitHub Repository](https://github.com/bearlike/Personal-Assistant)
diff --git a/meeseeks-chat/README.md b/meeseeks-chat/README.md
index 31a2396..8968338 100644
--- a/meeseeks-chat/README.md
+++ b/meeseeks-chat/README.md
@@ -1,5 +1,18 @@
-# meeseeks-chat
+# Meeseeks - Chat Interface
+
+
+
+
+
+
+
+
+
+
+
+
+- Chat Interface wrapped around the meeseeks-core. Powered by Streamlit.
+- For more information, such as installation, please check out the [Wiki](https://github.com/bearlike/Personal-Assistant/wiki).
-Chat Interface wrapped around the meeseeks-core. Powered by Streamlit.
[Link to GitHub](https://github.com/bearlike/Personal-Assistant)
diff --git a/meeseeks_ha_conversation/README.md b/meeseeks_ha_conversation/README.md
new file mode 100644
index 0000000..f84fcc8
--- /dev/null
+++ b/meeseeks_ha_conversation/README.md
@@ -0,0 +1,25 @@
+# Home Assistant Conversation Integration for Meeseeks 🚀
+
+
+
+
+
+
+
+
+
+ Answer questions and interpret sensor information |
+ Control devices and entities |
+
+
+ |
+ |
+
+
+
+- Home Assistant Conversation Integration for Meeseeks. Can be used with HA Assist ⭐.
+- Wrapped around the REST API Engine for Meeseeks. 100% coverage of Meeseeks API.
+- No components are explicitly tested for safety or security. Use with caution in a production environment.
+- For more information, such as installation, please check out the [Wiki](https://github.com/bearlike/Personal-Assistant/wiki).
+
+[Link to GitHub Repository](https://github.com/bearlike/Personal-Assistant)
diff --git a/meeseeks_ha_conversation/__init__.py b/meeseeks_ha_conversation/__init__.py
new file mode 100644
index 0000000..99bfb06
--- /dev/null
+++ b/meeseeks_ha_conversation/__init__.py
@@ -0,0 +1,168 @@
+"""Custom integration to integrate meeseeks_conversation with Home Assistant.
+
+For more details about this integration, please refer to
+https://github.com/bearlike/personal-Assistant/
+"""
+from __future__ import annotations
+
+from typing import Literal
+
+from homeassistant.components import conversation
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import MATCH_ALL
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
+from homeassistant.helpers import intent, template
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.util import ulid
+
+from .api import MeeseeksApiClient
+from .const import (
+ DOMAIN, LOGGER,
+ CONF_BASE_URL,
+ CONF_TIMEOUT,
+ DEFAULT_TIMEOUT,
+)
+# User-defined imports
+from .coordinator import MeeseeksDataUpdateCoordinator
+from .exceptions import (
+ ApiClientError,
+ ApiCommError,
+ ApiJsonError,
+ ApiTimeoutError
+)
+# from .helpers import get_exposed_entities
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up Meeseeks conversation using UI."""
+ # https://developers.home-assistant.io/docs/config_entries_index/#setting-up-an-entry
+ hass.data.setdefault(DOMAIN, {})
+ client = MeeseeksApiClient(
+ base_url=entry.data[CONF_BASE_URL],
+ timeout=entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT),
+ session=async_get_clientsession(hass),
+ )
+
+ hass.data[DOMAIN][entry.entry_id] = coordinator = MeeseeksDataUpdateCoordinator(
+ hass,
+ client,
+ )
+ # https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
+ await coordinator.async_config_entry_first_refresh()
+
+ try:
+ # TODO: Heartbeat check is not implemented but it is still wrapped.
+ response = await client.async_get_heartbeat()
+ if not response:
+ raise ApiClientError("Invalid Meeseeks server")
+ except ApiClientError as err:
+ raise ConfigEntryNotReady(err) from err
+
+ entry.async_on_unload(entry.add_update_listener(async_reload_entry))
+
+ conversation.async_set_agent(
+ hass, entry, MeeseeksAgent(hass, entry, client))
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload Meeseeks conversation."""
+ conversation.async_unset_agent(hass, entry)
+ return True
+
+
+async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
+ """Reload Meeseeks conversation."""
+ await async_unload_entry(hass, entry)
+ await async_setup_entry(hass, entry)
+
+
+class MeeseeksAgent(conversation.AbstractConversationAgent):
+ """Meeseeks conversation agent."""
+
+ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, client: MeeseeksApiClient) -> None:
+ """Initialize the agent."""
+ self.hass = hass
+ self.entry = entry
+ self.client = client
+ self.history: dict[str, dict] = {}
+
+ @property
+ def supported_languages(self) -> list[str] | Literal["*"]:
+ """Return a list of supported languages."""
+ return MATCH_ALL
+
+ async def async_process(
+ self, user_input: conversation.ConversationInput
+ ) -> conversation.ConversationResult:
+ """Process a sentence."""
+ # * If needeed in the future, uncomment the following lines
+ # raw_system_prompt = self.entry.options.get(
+ # CONF_PROMPT_SYSTEM, DEFAULT_PROMPT_SYSTEM)
+ # exposed_entities = get_exposed_entities(self.hass)
+ # ! Currently, history is not used but still implemented for future use
+ if user_input.conversation_id in self.history:
+ conversation_id = user_input.conversation_id
+ messages = self.history[conversation_id]
+ else:
+ conversation_id = ulid.ulid()
+ system_prompt = ""
+ messages = {
+ "system": system_prompt,
+ "context": None,
+ }
+
+ messages["prompt"] = user_input.text
+
+ try:
+ response = await self.query(messages)
+ except HomeAssistantError as err:
+ LOGGER.error("Something went wrong: %s", err)
+ intent_response = intent.IntentResponse(
+ language=user_input.language)
+ intent_response.async_set_error(
+ intent.IntentResponseErrorCode.UNKNOWN,
+ "Something went wrong, please check the logs for more information.",
+ )
+ return conversation.ConversationResult(
+ response=intent_response, conversation_id=conversation_id
+ )
+
+ messages["context"] = response["context"]
+ self.history[conversation_id] = messages
+
+ intent_response = intent.IntentResponse(language=user_input.language)
+ intent_response.async_set_speech(response["response"])
+ return conversation.ConversationResult(
+ response=intent_response, conversation_id=conversation_id
+ )
+
+ def _async_generate_prompt(self, raw_prompt: str, exposed_entities) -> str:
+ """Generate a prompt for the user."""
+ return template.Template(raw_prompt, self.hass).async_render(
+ {
+ "ha_name": self.hass.config.location_name,
+ "exposed_entities": exposed_entities,
+ },
+ parse_result=False,
+ )
+
+ async def query(
+ self,
+ messages
+ ):
+ """Process a sentence."""
+ # model = self.entry.options.get(CONF_MODEL, DEFAULT_MODEL)
+ # LOGGER.debug("Prompt for %s: %s", model, messages["prompt"])
+
+ # TODO: $context, and $system are not used but still implemented for
+ # future use
+ # * Generator
+ result = await self.client.async_generate({
+ "context": messages["context"],
+ "system": messages["system"],
+ "prompt": messages["prompt"],
+ })
+ response: str = result["task_result"]
+ LOGGER.debug("Response %s", response)
+ return result
diff --git a/meeseeks_ha_conversation/api.py b/meeseeks_ha_conversation/api.py
new file mode 100644
index 0000000..472f55a
--- /dev/null
+++ b/meeseeks_ha_conversation/api.py
@@ -0,0 +1,101 @@
+""" Meeseeks API Client. """
+from __future__ import annotations
+
+import aiohttp
+import async_timeout
+import json
+
+# User-defined imports
+from .exceptions import (
+ ApiClientError,
+ ApiCommError,
+ ApiJsonError,
+ ApiTimeoutError
+)
+from .const import LOGGER
+
+
+class MeeseeksApiClient:
+ """Meeseeks API Client."""
+
+ def __init__(
+ self,
+ base_url: str,
+ timeout: int,
+ session: aiohttp.ClientSession,
+ ) -> None:
+ """Sample API Client."""
+ self._base_url = base_url.rstrip("/")
+ self._api_key = 'msk-strong-password'
+ self.timeout = timeout
+ self._session = session
+
+ async def async_get_heartbeat(self) -> bool:
+ """Get heartbeat from the API."""
+ # TODO: Implement a heartbeat check
+ return True
+
+ async def async_get_models(self) -> any:
+ """Get models from the API."""
+ # TODO: This is monkey-patched for now
+ response_data = {
+ "models": [
+ {
+ "name": "meeseeks",
+ "modified_at": "2023-11-01T00:00:00.000000000-04:00",
+ "size": 0,
+ "digest": None
+ }
+ ]
+ }
+ return json.dumps(response_data)
+
+ async def async_generate(self, data: dict | None = None,) -> any:
+ """Generate a completion from the API."""
+ url_query = f"{self._base_url}/api/query"
+ data_custom = {
+ 'query': str(data["prompt"]).strip(),
+ }
+ # Pass headers as None to use the default headers
+ return await self._meeseeks_api_wrapper(
+ method="post",
+ url=url_query,
+ data=data_custom,
+ headers=None,
+ )
+
+ async def _meeseeks_api_wrapper(
+ self,
+ method: str,
+ url: str,
+ data: dict | None = None,
+ headers: dict | None = None,
+ decode_json: bool = True,
+ ) -> any:
+ """Get information from the API."""
+ if headers is None:
+ headers = {
+ 'accept': 'application/json',
+ 'X-API-KEY': self._api_key,
+ 'Content-Type': 'application/json',
+ }
+ async with async_timeout.timeout(self.timeout):
+ response = await self._session.request(
+ method=method,
+ url=url,
+ headers=headers,
+ json=data,
+ )
+ response.raise_for_status()
+
+ if decode_json:
+ response_data = await response.json()
+ if response.status == 404:
+ raise ApiJsonError(response_data["error"])
+ LOGGER.debug(f"Response data: {response_data}")
+ response_data["response"] = response_data["task_result"]
+ response_data["context"] = response_data["task_result"]
+ return response_data
+ else:
+ LOGGER.debug("Fallback to text response")
+ return await response.text()
diff --git a/meeseeks_ha_conversation/config_flow.py b/meeseeks_ha_conversation/config_flow.py
new file mode 100644
index 0000000..baa06ad
--- /dev/null
+++ b/meeseeks_ha_conversation/config_flow.py
@@ -0,0 +1,198 @@
+"""Adds config flow for Meeseeks."""
+from __future__ import annotations
+
+import types
+from typing import Any
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.data_entry_flow import FlowResult
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_create_clientsession
+from homeassistant.helpers.selector import (
+ NumberSelector,
+ NumberSelectorConfig,
+ TemplateSelector,
+ SelectSelector,
+ SelectSelectorConfig,
+ SelectSelectorMode,
+ SelectOptionDict
+)
+# User-defined imports
+from .api import MeeseeksApiClient
+from .const import (
+ DOMAIN, LOGGER,
+ MENU_OPTIONS,
+
+ CONF_BASE_URL,
+ CONF_API_KEY,
+ CONF_TIMEOUT,
+ CONF_MODEL,
+ CONF_CTX_SIZE,
+ CONF_MAX_TOKENS,
+ CONF_MIROSTAT_MODE,
+ CONF_MIROSTAT_ETA,
+ CONF_MIROSTAT_TAU,
+ CONF_TEMPERATURE,
+ CONF_REPEAT_PENALTY,
+ CONF_TOP_K,
+ CONF_TOP_P,
+ CONF_PROMPT_SYSTEM,
+
+ DEFAULT_BASE_URL,
+ DEFAULT_API_KEY,
+ DEFAULT_TIMEOUT,
+ DEFAULT_MODEL,
+ DEFAULT_CTX_SIZE,
+ DEFAULT_MAX_TOKENS,
+ DEFAULT_MIROSTAT_MODE,
+ DEFAULT_MIROSTAT_ETA,
+ DEFAULT_MIROSTAT_TAU,
+ DEFAULT_TEMPERATURE,
+ DEFAULT_REPEAT_PENALTY,
+ DEFAULT_TOP_K,
+ DEFAULT_TOP_P,
+ DEFAULT_PROMPT_SYSTEM
+)
+from .exceptions import (
+ ApiClientError,
+ ApiCommError,
+ ApiTimeoutError
+)
+
+
+STEP_USER_DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_BASE_URL, default=DEFAULT_BASE_URL): str,
+ vol.Required(CONF_API_KEY, default=DEFAULT_API_KEY): str,
+ vol.Required(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): int,
+ }
+)
+
+DEFAULT_OPTIONS = types.MappingProxyType(
+ {
+ CONF_BASE_URL: DEFAULT_BASE_URL,
+ CONF_API_KEY: DEFAULT_API_KEY,
+ CONF_TIMEOUT: DEFAULT_TIMEOUT,
+ CONF_MODEL: DEFAULT_MODEL,
+ CONF_PROMPT_SYSTEM: DEFAULT_PROMPT_SYSTEM
+ }
+)
+
+
+class MeeseeksConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Meeseeks Conversation. Handles UI wizard."""
+
+ VERSION = 1
+
+ async def async_step_user(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Handle the initial step."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id="user", data_schema=STEP_USER_DATA_SCHEMA
+ )
+
+ # Search for duplicates with the same CONF_BASE_URL value.
+ for existing_entry in self._async_current_entries(include_ignore=False):
+ if existing_entry.data.get(CONF_BASE_URL) == user_input[CONF_BASE_URL]:
+ return self.async_abort(reason="already_configured")
+
+ errors = {}
+ try:
+ self.client = MeeseeksApiClient(
+ base_url=cv.url_no_path(user_input[CONF_BASE_URL]),
+ timeout=user_input[CONF_TIMEOUT],
+ session=async_create_clientsession(self.hass),
+ )
+ response = await self.client.async_get_heartbeat()
+ if not response:
+ raise vol.Invalid("Invalid Meeseeks server")
+ # except vol.Invalid:
+ # errors["base"] = "invalid_url"
+ # except ApiTimeoutError:
+ # errors["base"] = "timeout_connect"
+ # except ApiCommError:
+ # errors["base"] = "cannot_connect"
+ # except ApiClientError as exception:
+ # LOGGER.exception("Unexpected exception: %s", exception)
+ # errors["base"] = "unknown"
+ except Exception as exception:
+ LOGGER.exception("Unexpected exception: %s", exception)
+ errors["base"] = "unknown"
+ else:
+ return self.async_create_entry(
+ title=f"Meeseeks - {user_input[CONF_BASE_URL]}",
+ data={
+ CONF_BASE_URL: user_input[CONF_BASE_URL]
+ },
+ options={
+ CONF_TIMEOUT: user_input[CONF_TIMEOUT]
+ }
+ )
+
+ return self.async_show_form(
+ step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
+ )
+
+ @staticmethod
+ def async_get_options_flow(
+ config_entry: config_entries.ConfigEntry,
+ ) -> config_entries.OptionsFlow:
+ """Create the options flow."""
+ return MeeseeksOptionsFlow(config_entry)
+
+
+class MeeseeksOptionsFlow(config_entries.OptionsFlow):
+ """Meeseeks config flow options handler."""
+
+ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
+ """Initialize options flow."""
+ self.config_entry = config_entry
+ self.options = dict(config_entry.options)
+
+ async def async_step_init(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Manage the options."""
+ return self.async_show_menu(
+ step_id="init",
+ menu_options=MENU_OPTIONS
+ )
+
+ async def async_step_all_set(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Manage the options."""
+ return self.async_show_menu(
+ step_id="init",
+ menu_options=MENU_OPTIONS
+ )
+
+ async def async_step_general_config(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Manage the options."""
+ return self.async_show_menu(
+ step_id="init",
+ menu_options=MENU_OPTIONS
+ )
+
+ async def async_step_prompt_system(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Manage the options."""
+ return self.async_show_menu(
+ step_id="init",
+ menu_options=MENU_OPTIONS
+ )
+
+ async def async_step_model_config(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Manage the options."""
+ return self.async_show_menu(
+ step_id="init",
+ menu_options=MENU_OPTIONS
+ )
diff --git a/meeseeks_ha_conversation/const.py b/meeseeks_ha_conversation/const.py
new file mode 100644
index 0000000..0d7e456
--- /dev/null
+++ b/meeseeks_ha_conversation/const.py
@@ -0,0 +1,41 @@
+"""Constants for meeseeks_conversation."""
+from logging import Logger, getLogger
+
+LOGGER: Logger = getLogger(__package__)
+
+NAME = "Meeseeks"
+DOMAIN = "meeseeks_conversation"
+
+MENU_OPTIONS = ["all_set"]
+# MENU_OPTIONS = ["general_config", "model_config", "prompt_system"]
+
+CONF_BASE_URL = "base_url"
+CONF_API_KEY = "api_key"
+CONF_TIMEOUT = "timeout"
+CONF_MODEL = "chat_model"
+CONF_CTX_SIZE = "ctx_size"
+CONF_MAX_TOKENS = "max_tokens"
+CONF_MIROSTAT_MODE = "mirostat_mode"
+CONF_MIROSTAT_ETA = "mirostat_eta"
+CONF_MIROSTAT_TAU = "mirostat_tau"
+CONF_TEMPERATURE = "temperature"
+CONF_REPEAT_PENALTY = "repeat_penalty"
+CONF_TOP_K = "top_k"
+CONF_TOP_P = "top_p"
+CONF_PROMPT_SYSTEM = "prompt"
+
+DEFAULT_BASE_URL = "http://meeseeks.server:5123"
+DEFAULT_API_KEY = "msk-strong-password"
+DEFAULT_TIMEOUT = 60
+DEFAULT_MODEL = "llama2:latest"
+DEFAULT_CTX_SIZE = 2048
+DEFAULT_MAX_TOKENS = 128
+DEFAULT_MIROSTAT_MODE = "0"
+DEFAULT_MIROSTAT_ETA = 0.1
+DEFAULT_MIROSTAT_TAU = 5.0
+DEFAULT_TEMPERATURE = 0.8
+DEFAULT_REPEAT_PENALTY = 1.1
+DEFAULT_TOP_K = 40
+DEFAULT_TOP_P = 0.9
+
+DEFAULT_PROMPT_SYSTEM = ""
diff --git a/meeseeks_ha_conversation/coordinator.py b/meeseeks_ha_conversation/coordinator.py
new file mode 100644
index 0000000..f4d791a
--- /dev/null
+++ b/meeseeks_ha_conversation/coordinator.py
@@ -0,0 +1,43 @@
+"""DataUpdateCoordinator for meeseeks_conversation."""
+from __future__ import annotations
+
+from datetime import timedelta
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.update_coordinator import (
+ DataUpdateCoordinator,
+ UpdateFailed,
+)
+
+from .api import MeeseeksApiClient
+from .const import DOMAIN, LOGGER
+from .exceptions import ApiClientError
+
+
+# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
+class MeeseeksDataUpdateCoordinator(DataUpdateCoordinator):
+ """Class to manage fetching data from the API."""
+
+ config_entry: ConfigEntry
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ client: MeeseeksApiClient,
+ ) -> None:
+ """Initialize."""
+ self.client = client
+ super().__init__(
+ hass=hass,
+ logger=LOGGER,
+ name=DOMAIN,
+ update_interval=timedelta(minutes=5),
+ )
+
+ async def _async_update_data(self):
+ """Update data via library."""
+ try:
+ return await self.client.async_get_heartbeat()
+ except ApiClientError as exception:
+ raise UpdateFailed(exception) from exception
diff --git a/meeseeks_ha_conversation/exceptions.py b/meeseeks_ha_conversation/exceptions.py
new file mode 100644
index 0000000..e0c898e
--- /dev/null
+++ b/meeseeks_ha_conversation/exceptions.py
@@ -0,0 +1,14 @@
+"""The exceptions used by Extended OpenAI Conversation."""
+from homeassistant.exceptions import HomeAssistantError
+
+class ApiClientError(HomeAssistantError):
+ """Exception to indicate a general API error."""
+
+class ApiCommError(ApiClientError):
+ """Exception to indicate a communication error."""
+
+class ApiJsonError(ApiClientError):
+ """Exception to indicate an error with json response."""
+
+class ApiTimeoutError(ApiClientError):
+ """Exception to indicate a timeout error."""
diff --git a/meeseeks_ha_conversation/helpers.py b/meeseeks_ha_conversation/helpers.py
new file mode 100644
index 0000000..ab08498
--- /dev/null
+++ b/meeseeks_ha_conversation/helpers.py
@@ -0,0 +1,24 @@
+"""Helper functions for Meeseeks."""
+
+from homeassistant.components.conversation import DOMAIN as CONVERSATION_DOMAIN
+from homeassistant.components.homeassistant.exposed_entities import async_should_expose
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry
+
+
+def get_exposed_entities(hass: HomeAssistant) -> list[dict]:
+ """Return exposed entities."""
+ hass_entity = entity_registry.async_get(hass)
+ exposed_entities: list[dict] = []
+
+ for state in hass.states.async_all():
+ if async_should_expose(hass, CONVERSATION_DOMAIN, state.entity_id):
+ entity = hass_entity.async_get(state.entity_id)
+ exposed_entities.append({
+ "entity_id": state.entity_id,
+ "name": state.name,
+ "state": state.state,
+ "aliases": entity.aliases if entity else [],
+ })
+
+ return exposed_entities
diff --git a/meeseeks_ha_conversation/manifest.json b/meeseeks_ha_conversation/manifest.json
new file mode 100644
index 0000000..e506dab
--- /dev/null
+++ b/meeseeks_ha_conversation/manifest.json
@@ -0,0 +1,16 @@
+{
+ "domain": "meeseeks_conversation",
+ "name": "Meeseeks",
+ "codeowners": [
+ "@bearlike"
+ ],
+ "config_flow": true,
+ "dependencies": [
+ "conversation"
+ ],
+ "documentation": "https://github.com/bearlike/Personal-Assistant",
+ "integration_type": "service",
+ "iot_class": "cloud_polling",
+ "issue_tracker": "https://github.com/bearlike/Personal-Assistant/issues",
+ "version": "v1.2.0"
+}
diff --git a/meeseeks_ha_conversation/strings.json b/meeseeks_ha_conversation/strings.json
new file mode 100644
index 0000000..06bc60e
--- /dev/null
+++ b/meeseeks_ha_conversation/strings.json
@@ -0,0 +1,42 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "base_url": "Base URL",
+ "api_key": "API Key",
+ "timeout": "API Timeout"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "Unable to connect",
+ "invalid_url": "Invalid URL",
+ "timeout_connect": "Timeout while establishing connection",
+ "unknown": "Unexpected error, please check logs"
+ },
+ "abort": {
+ "already_configured": "Service is already exist and configured"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "menu_options": {
+ "general_config": "General Settings",
+ "prompt_system": "System Prompt",
+ "all_set": "Nothing to configure. You're all set!"
+ }
+ },
+ "general_config": {
+ "title": "General Settings",
+ "data": {
+ "timeout": "API Timeout"
+ }
+ },
+ "prompt_system": {
+ "title": "System Prompt"
+ }
+ }
+ }
+}
diff --git a/meeseeks_ha_conversation/translations/en.json b/meeseeks_ha_conversation/translations/en.json
new file mode 100644
index 0000000..eb37c1e
--- /dev/null
+++ b/meeseeks_ha_conversation/translations/en.json
@@ -0,0 +1,58 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "base_url": "Base URL",
+ "api_key": "API Key",
+ "timeout": "API Timeout"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_url": "Invalid URL",
+ "timeout_connect": "Timeout establishing connection",
+ "unknown": "Unexpected error, check logs"
+ },
+ "abort": {
+ "already_configured": "Service is already configured"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "menu_options": {
+ "general_config": "General Settings",
+ "model_config": "Model Configuration",
+ "prompt_system": "System Prompt",
+ "all_set": "Nothing to configure. You're all set!"
+ }
+ },
+ "general_config": {
+ "title": "General Settings",
+ "data": {
+ "timeout": "API Timeout"
+ }
+ },
+ "model_config": {
+ "title": "Model Configuration",
+ "data": {
+ "chat_model": "Model",
+ "ctx_size": "Context Size",
+ "max_tokens": "Maximum Tokens",
+ "mirostat_mode": "Mirostat Mode",
+ "mirostat_eta": "Mirostat ETA",
+ "mirostat_tau": "Mirostat TAU",
+ "repeat_penalty": "Repeat Penalty",
+ "temperature": "Temperature",
+ "top_p": "Top P",
+ "top_k": "Top K"
+ }
+ },
+ "prompt_system": {
+ "title": "System Prompt"
+ }
+ }
+ }
+}