<commit_before>"""Support for Snips on-device ASR and NLU."""
import json
import logging
from datetime import timedelta

import voluptuous as vol

from homeassistant.core import callback
from homeassistant.helpers import intent, config_validation as cv
from homeassistant.components import mqtt

DOMAIN = "snips"
CONF_INTENTS = "intents"
CONF_ACTION = "action"
CONF_FEEDBACK = "feedback_sounds"
CONF_PROBABILITY = "probability_threshold"
CONF_SITE_IDS = "site_ids"

SERVICE_SAY = "say"
SERVICE_SAY_ACTION = "say_action"
SERVICE_FEEDBACK_ON = "feedback_on"
SERVICE_FEEDBACK_OFF = "feedback_off"

INTENT_TOPIC = "hermes/intent/#"
FEEDBACK_ON_TOPIC = "hermes/feedback/sound/toggleOn"
FEEDBACK_OFF_TOPIC = "hermes/feedback/sound/toggleOff"

ATTR_TEXT = "text"
ATTR_SITE_ID = "site_id"
ATTR_CUSTOM_DATA = "custom_data"
ATTR_CAN_BE_ENQUEUED = "can_be_enqueued"
ATTR_INTENT_FILTER = "intent_filter"

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema(
    {
        DOMAIN: vol.Schema(
            {
                vol.Optional(CONF_FEEDBACK): cv.boolean,
                vol.Optional(CONF_PROBABILITY, default=0): vol.Coerce(float),
                vol.Optional(CONF_SITE_IDS, default=["default"]): vol.All(
                    cv.ensure_list, [cv.string]
                ),
            }
        )
    },
    extra=vol.ALLOW_EXTRA,
)

INTENT_SCHEMA = vol.Schema(
    {
        vol.Required("input"): str,
        vol.Required("intent"): {vol.Required("intentName"): str},
        vol.Optional("slots"): [
            {
                vol.Required("slotName"): str,
                vol.Required("value"): {
                    vol.Required("kind"): str,
                    vol.Optional("value"): cv.match_all,
                    vol.Optional("rawValue"): cv.match_all,
                },
            }
        ],
    },
    extra=vol.ALLOW_EXTRA,
)

SERVICE_SCHEMA_SAY = vol.Schema(
    {
        vol.Required(ATTR_TEXT): str,
        vol.Optional(ATTR_SITE_ID, default="default"): str,
        vol.Optional(ATTR_CUSTOM_DATA, default=""): str,
    }
)
SERVICE_SCHEMA_SAY_ACTION = vol.Schema(
    {
        vol.Required(ATTR_TEXT): str,
        vol.Optional(ATTR_SITE_ID, default="default"): str,
        vol.Optional(ATTR_CUSTOM_DATA, default=""): str,
        vol.Optional(ATTR_CAN_BE_ENQUEUED, default=True): cv.boolean,
        vol.Optional(ATTR_INTENT_FILTER): vol.All(cv.ensure_list),
    }
)
SERVICE_SCHEMA_FEEDBACK = vol.Schema(
    {vol.Optional(ATTR_SITE_ID, default="default"): str}
)


async def async_setup(hass, config):
    """Activate Snips component."""

    @callback
    def async_set_feedback(site_ids, state):
        """Set Feedback sound state."""
        site_ids = site_ids if site_ids else config[DOMAIN].get(CONF_SITE_IDS)
        topic = FEEDBACK_ON_TOPIC if state else FEEDBACK_OFF_TOPIC
        for site_id in site_ids:
            payload = json.dumps({"siteId": site_id})
            hass.components.mqtt.async_publish(
                FEEDBACK_ON_TOPIC, None, qos=0, retain=False
            )
            hass.components.mqtt.async_publish(
                topic, payload, qos=int(state), retain=state
            )

    if CONF_FEEDBACK in config[DOMAIN]:
        async_set_feedback(None, config[DOMAIN][CONF_FEEDBACK])

    async def message_received(msg):
        """Handle new messages on MQTT."""
        _LOGGER.debug("New intent: %s", msg.payload)

        try:
            request = json.loads(msg.payload)
        except TypeError:
            _LOGGER.error("Received invalid JSON: %s", msg.payload)
            return

        if request["intent"]["confidenceScore"] < config[DOMAIN].get(CONF_PROBABILITY):
            _LOGGER.warning(
                "Intent below probaility threshold %s < %s",
                request["intent"]["confidenceScore"],
                config[DOMAIN].get(CONF_PROBABILITY),
            )
            return

        try:
            request = INTENT_SCHEMA(request)
        except vol.Invalid as err:
            _LOGGER.error("Intent has invalid schema: %s. %s", err, request)
            return

        if request["intent"]["intentName"].startswith("user_"):
            intent_type = request["intent"]["intentName"].split("__")[-1]
        else:
            intent_type = request["intent"]["intentName"].split(":")[-1]
        snips_response = None
        slots = {}
        for slot in request.get("slots", []):
            slots[slot["slotName"]] = {"value": resolve_slot_values(slot)}
            slots["{}_raw".format(slot["slotName"])] = {"value": slot["rawValue"]}
        slots["site_id"] = {"value": request.get("siteId")}
        slots["session_id"] = {"value": request.get("sessionId")}
        slots["confidenceScore"] = {"value": request["intent"]["confidenceScore"]}

        try:
            intent_response = await intent.async_handle(
                hass, DOMAIN, intent_type, slots, request["input"]
            )
            if "plain" in intent_response.speech:
                snips_response = intent_response.speech["plain"]["speech"]
        except intent.UnknownIntent:
            _LOGGER.warning(
                "Received unknown intent %s", request["intent"]["intentName"]
            )
        except intent.IntentError:
            _LOGGER.exception("Error while handling intent: %s.", intent_type)

        if snips_response:
            notification = {
                "sessionId": request.get("sessionId", "default"),
                "text": snips_response,
            }

            _LOGGER.debug("send_response %s", json.dumps(notification))
            mqtt.async_publish(
                hass, "hermes/dialogueManager/endSession", json.dumps(notification)
            )

    await hass.components.mqtt.async_subscribe(INTENT_TOPIC, message_received)

    async def snips_say(call):
        """Send a Snips notification message."""
        notification = {
            "siteId": call.data.get(ATTR_SITE_ID, "default"),
            "customData": call.data.get(ATTR_CUSTOM_DATA, ""),
            "init": {"type": "notification", "text": call.data.get(ATTR_TEXT)},
        }<commit_msg>Make intents end Snips session without speech (#28820)<commit_after>"""Support for Snips on-device ASR and NLU."""
import json
import logging
from datetime import timedelta

import voluptuous as vol

from homeassistant.core import callback
from homeassistant.helpers import intent, config_validation as cv
from homeassistant.components import mqtt

DOMAIN = "snips"
CONF_INTENTS = "intents"
CONF_ACTION = "action"
CONF_FEEDBACK = "feedback_sounds"
CONF_PROBABILITY = "probability_threshold"
CONF_SITE_IDS = "site_ids"

SERVICE_SAY = "say"
SERVICE_SAY_ACTION = "say_action"
SERVICE_FEEDBACK_ON = "feedback_on"
SERVICE_FEEDBACK_OFF = "feedback_off"

INTENT_TOPIC = "hermes/intent/#"
FEEDBACK_ON_TOPIC = "hermes/feedback/sound/toggleOn"
FEEDBACK_OFF_TOPIC = "hermes/feedback/sound/toggleOff"

ATTR_TEXT = "text"
ATTR_SITE_ID = "site_id"
ATTR_CUSTOM_DATA = "custom_data"
ATTR_CAN_BE_ENQUEUED = "can_be_enqueued"
ATTR_INTENT_FILTER = "intent_filter"

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema(
    {
        DOMAIN: vol.Schema(
            {
                vol.Optional(CONF_FEEDBACK): cv.boolean,
                vol.Optional(CONF_PROBABILITY, default=0): vol.Coerce(float),
                vol.Optional(CONF_SITE_IDS, default=["default"]): vol.All(
                    cv.ensure_list, [cv.string]
                ),
            }
        )
    },
    extra=vol.ALLOW_EXTRA,
)

INTENT_SCHEMA = vol.Schema(
    {
        vol.Required("input"): str,
        vol.Required("intent"): {vol.Required("intentName"): str},
        vol.Optional("slots"): [
            {
                vol.Required("slotName"): str,
                vol.Required("value"): {
                    vol.Required("kind"): str,
                    vol.Optional("value"): cv.match_all,
                    vol.Optional("rawValue"): cv.match_all,
                },
            }
        ],
    },
    extra=vol.ALLOW_EXTRA,
)

SERVICE_SCHEMA_SAY = vol.Schema(
    {
        vol.Required(ATTR_TEXT): str,
        vol.Optional(ATTR_SITE_ID, default="default"): str,
        vol.Optional(ATTR_CUSTOM_DATA, default=""): str,
    }
)
SERVICE_SCHEMA_SAY_ACTION = vol.Schema(
    {
        vol.Required(ATTR_TEXT): str,
        vol.Optional(ATTR_SITE_ID, default="default"): str,
        vol.Optional(ATTR_CUSTOM_DATA, default=""): str,
        vol.Optional(ATTR_CAN_BE_ENQUEUED, default=True): cv.boolean,
        vol.Optional(ATTR_INTENT_FILTER): vol.All(cv.ensure_list),
    }
)
SERVICE_SCHEMA_FEEDBACK = vol.Schema(
    {vol.Optional(ATTR_SITE_ID, default="default"): str}
)


async def async_setup(hass, config):
    """Activate Snips component."""

    @callback
    def async_set_feedback(site_ids, state):
        """Set Feedback sound state."""
        site_ids = site_ids if site_ids else config[DOMAIN].get(CONF_SITE_IDS)
        topic = FEEDBACK_ON_TOPIC if state else FEEDBACK_OFF_TOPIC
        for site_id in site_ids:
            payload = json.dumps({"siteId": site_id})
            hass.components.mqtt.async_publish(
                FEEDBACK_ON_TOPIC, None, qos=0, retain=False
            )
            hass.components.mqtt.async_publish(
                topic, payload, qos=int(state), retain=state
            )

    if CONF_FEEDBACK in config[DOMAIN]:
        async_set_feedback(None, config[DOMAIN][CONF_FEEDBACK])

    async def message_received(msg):
        """Handle new messages on MQTT."""
        _LOGGER.debug("New intent: %s", msg.payload)

        try:
            request = json.loads(msg.payload)
        except TypeError:
            _LOGGER.error("Received invalid JSON: %s", msg.payload)
            return

        if request["intent"]["confidenceScore"] < config[DOMAIN].get(CONF_PROBABILITY):
            _LOGGER.warning(
                "Intent below probaility threshold %s < %s",
                request["intent"]["confidenceScore"],
                config[DOMAIN].get(CONF_PROBABILITY),
            )
            return

        try:
            request = INTENT_SCHEMA(request)
        except vol.Invalid as err:
            _LOGGER.error("Intent has invalid schema: %s. %s", err, request)
            return

        if request["intent"]["intentName"].startswith("user_"):
            intent_type = request["intent"]["intentName"].split("__")[-1]
        else:
            intent_type = request["intent"]["intentName"].split(":")[-1]
        slots = {}
        for slot in request.get("slots", []):
            slots[slot["slotName"]] = {"value": resolve_slot_values(slot)}
            slots["{}_raw".format(slot["slotName"])] = {"value": slot["rawValue"]}
        slots["site_id"] = {"value": request.get("siteId")}
        slots["session_id"] = {"value": request.get("sessionId")}
        slots["confidenceScore"] = {"value": request["intent"]["confidenceScore"]}

        try:
            intent_response = await intent.async_handle(
                hass, DOMAIN, intent_type, slots, request["input"]
            )
            notification = {"sessionId": request.get("sessionId", "default")}

            if "plain" in intent_response.speech:
                notification["text"] = intent_response.speech["plain"]["speech"]

            _LOGGER.debug("send_response %s", json.dumps(notification))
            mqtt.async_publish(
                hass, "hermes/dialogueManager/endSession", json.dumps(notification)
            )
        except intent.UnknownIntent:
            _LOGGER.warning(
                "Received unknown intent %s", request["intent"]["intentName"]
            )
        except intent.IntentError:
            _LOGGER.exception("Error while handling intent: %s.", intent_type)

    await hass.components.mqtt.async_subscribe(INTENT_TOPIC, message_received)

    async def snips_say(call):
        """Send a Snips notification message."""
        notification = {
            "siteId": call.data.get(ATTR_SITE_ID, "default"),
            "customData": call.data.get(ATTR_CUSTOM_DATA, ""),
            "init": {"type": "notification", "text": call.data.get(ATTR_TEXT)},
        }
{'fraction_non_alphanumeric': 0.12713050638122683, 'fraction_numerical': 0.0010703993412927131, 'mean_word_length': 2.538013399359161, 'pattern_counts': {'":': 27, '<': 7, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5384145', 'n_tokens_mistral': 3748, 'n_tokens_neox': 3743, 'n_words': 669}