-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
📦 NEW: Home Assistant Conversation Integration for Meeseeks
- Loading branch information
Showing
16 changed files
with
772 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,13 @@ | ||
# meeseeks-api | ||
# Meeseeks API Server | ||
<p align="center"> | ||
<a href="https://github.com/bearlike/Personal-Assistant/wiki"><img alt="Wiki" src="https://img.shields.io/badge/GitHub-Wiki-blue?style=for-the-badge&logo=github"></a> | ||
<a href="https://github.com/bearlike/Personal-Assistant/pkgs/container/meeseeks-chat"><img src="https://img.shields.io/badge/ghcr.io-bearlike/meeseeks−api:latest-blue?style=for-the-badge&logo=docker&logoColor=white" alt="Docker Image"></a> | ||
<a href="https://github.com/bearlike/Personal-Assistant/releases"><img src="https://img.shields.io/github/v/release/bearlike/Personal-Assistant?style=for-the-badge&" alt="GitHub Release"></a> | ||
</p> | ||
|
||
- 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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,18 @@ | ||
# meeseeks-chat | ||
# Meeseeks - Chat Interface | ||
<p align="center"> | ||
<a href="https://github.com/bearlike/Personal-Assistant/wiki"><img alt="Wiki" src="https://img.shields.io/badge/GitHub-Wiki-blue?style=for-the-badge&logo=github"></a> | ||
<a href="https://github.com/bearlike/Personal-Assistant/pkgs/container/meeseeks-chat"><img src="https://img.shields.io/badge/ghcr.io-bearlike/meeseeks−chat:latest-blue?style=for-the-badge&logo=docker&logoColor=white" alt="Docker Image"></a> | ||
<a href="https://github.com/bearlike/Personal-Assistant/releases"><img src="https://img.shields.io/github/v/release/bearlike/Personal-Assistant?style=for-the-badge&" alt="GitHub Release"></a> | ||
</p> | ||
|
||
|
||
<p align="center"> | ||
<img src="../docs/screenshot_chat_app_1.png" alt="Screenshot of Meeseks WebUI" height="512px"> | ||
</p> | ||
|
||
|
||
- 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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# Home Assistant Conversation Integration for Meeseeks 🚀 | ||
|
||
<p align="center"> | ||
<a href="https://github.com/bearlike/Personal-Assistant/wiki"><img alt="Wiki" src="https://img.shields.io/badge/GitHub-Wiki-blue?style=for-the-badge&logo=github"></a> | ||
<a href="https://github.com/bearlike/Personal-Assistant/releases"><img src="https://img.shields.io/github/v/release/bearlike/Personal-Assistant?style=for-the-badge&" alt="GitHub Release"></a> | ||
</p> | ||
|
||
|
||
<table align="center"> | ||
<tr> | ||
<th>Answer questions and interpret sensor information</th> | ||
<th>Control devices and entities</th> | ||
</tr> | ||
<tr> | ||
<td align="center"><img src="../docs/screenshot_ha_assist_1.png" alt="Screenshot" height="512px"></td> | ||
<td align="center"><img src="../docs/screenshot_ha_assist_2.png" alt="Screenshot" height="512px"></td> | ||
</tr> | ||
</table> | ||
|
||
- 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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
Oops, something went wrong.