snips-skill 0.1.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,59 @@
1
+ from .exceptions import SnipsClarificationError, SnipsError
2
+ from .i18n import CONFIRMATIONS, get_translations
3
+ from .log import LoggingMixin
4
+ from .mqtt import CommandLineClient, MqttClient, topic
5
+ from .multi_room import ROOMS, MultiRoomConfig, room_with_article, room_with_preposition
6
+ from .skill import PARDON, Skill, intent, min_confidence, require_slot
7
+ from .snips import (
8
+ SnipsClient,
9
+ debug_json,
10
+ on_continue_session,
11
+ on_end_session,
12
+ on_hotword_detected,
13
+ on_intent,
14
+ on_play_finished,
15
+ on_session_ended,
16
+ on_session_started,
17
+ on_start_session,
18
+ )
19
+ from .state import StateAwareMixin, conditional, when
20
+ from .tasks import Scheduler, cron, delay, now
21
+
22
+ __version__ = "0.1.29"
23
+
24
+ __all__ = (
25
+ "CommandLineClient",
26
+ "conditional",
27
+ "CONFIRMATIONS",
28
+ "cron",
29
+ "debug_json",
30
+ "delay",
31
+ "get_translations",
32
+ "intent",
33
+ "LoggingMixin",
34
+ "min_confidence",
35
+ "MqttClient",
36
+ "MultiRoomConfig",
37
+ "now",
38
+ "on_continue_session",
39
+ "on_end_session",
40
+ "on_hotword_detected",
41
+ "on_intent",
42
+ "on_play_finished",
43
+ "on_session_ended",
44
+ "on_session_started",
45
+ "on_start_session",
46
+ "PARDON",
47
+ "require_slot",
48
+ "room_with_article",
49
+ "room_with_preposition",
50
+ "ROOMS",
51
+ "Scheduler",
52
+ "Skill",
53
+ "SnipsClarificationError",
54
+ "SnipsClient",
55
+ "SnipsError",
56
+ "StateAwareMixin",
57
+ "topic",
58
+ "when",
59
+ )
@@ -0,0 +1,28 @@
1
+ import logging
2
+
3
+ from basecmd import BaseCmd
4
+
5
+ from .intent import IntentPayload
6
+ from .log import LoggingMixin
7
+ from .snips import SnipsClient, on_continue_session, on_end_session, on_intent
8
+
9
+
10
+ class Logger(BaseCmd, LoggingMixin, SnipsClient):
11
+ @on_intent("#")
12
+ # Do not use @intent here because it ends the session,
13
+ # and thus interferes with other intent handlers
14
+ def intent_logger(self, userdata, msg):
15
+ self.log_intent(IntentPayload.model_validate(msg.payload), level=logging.INFO)
16
+
17
+ @on_end_session()
18
+ @on_continue_session()
19
+ def response_logger(self, userdata, msg):
20
+ self.log_response(msg.payload.get("text"), level=logging.INFO)
21
+
22
+
23
+ def main():
24
+ Logger().run()
25
+
26
+
27
+ if __name__ == "__main__": # demo code
28
+ main()
@@ -0,0 +1,60 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+ from pydantic.alias_generators import to_camel
5
+
6
+
7
+ class ActionInit(
8
+ BaseModel, alias_generator=to_camel, serialize_by_alias=True, validate_by_name=True
9
+ ):
10
+ "Start a session with an action"
11
+
12
+ # See: https://docs.snips.ai/reference/dialogue#session-initialization-action
13
+ type: Literal["action"] = "action"
14
+ text: str | None = None
15
+ intent_filter: list[str] | None = None
16
+ can_be_enqueued: bool = True
17
+ send_intent_not_recognized: bool = False
18
+
19
+
20
+ class NotififationInit(BaseModel):
21
+ "Start a session with a notification"
22
+
23
+ # See: https://docs.snips.ai/reference/dialogue#session-initialization-notification
24
+ type: Literal["notification"] = "notification"
25
+ text: str
26
+
27
+
28
+ class StartSession(
29
+ BaseModel, alias_generator=to_camel, serialize_by_alias=True, validate_by_name=True
30
+ ):
31
+ "Start a session with an optional message"
32
+
33
+ # See: https://docs.snips.ai/reference/dialogue#start-session
34
+ site_id: str
35
+ init: ActionInit | NotififationInit = Field(discriminator="type")
36
+ custom_data: str | None = None
37
+
38
+
39
+ class EndSession(
40
+ BaseModel, alias_generator=to_camel, serialize_by_alias=True, validate_by_name=True
41
+ ):
42
+ "End the session with an optional message"
43
+
44
+ # See: https://docs.snips.ai/reference/dialogue#end-session
45
+ session_id: str
46
+ text: str | None = None
47
+
48
+
49
+ class ContinueSession(
50
+ BaseModel, alias_generator=to_camel, serialize_by_alias=True, validate_by_name=True
51
+ ):
52
+ "Continue the session with a question"
53
+
54
+ # See: https://docs.snips.ai/reference/dialogue#continue-session
55
+ session_id: str
56
+ text: str
57
+ intent_filter: list[str] | None = None
58
+ slot: str | None = None
59
+ send_intent_not_recognized: bool = False
60
+ custom_data: str | None = None
@@ -0,0 +1,34 @@
1
+ class SnipsError(Exception):
2
+ "Signal that an intent cannot be handled"
3
+
4
+ def __str__(self):
5
+ return str(self.args[0])
6
+
7
+
8
+ class SnipsClarificationError(SnipsError):
9
+ "Signal that some clarification is needed from the user."
10
+
11
+ def __init__(self, msg, intent=None, slot=None, custom_data=None):
12
+ """
13
+ Create a clarification request.
14
+ :param msg: Question for the user
15
+ :param intent: Optional expected intent
16
+ :param slot: Optional slot name
17
+ See: https://docs.snips.ai/reference/dialogue#continue-session
18
+ """
19
+ super().__init__(msg, intent, slot, custom_data)
20
+
21
+ def __str__(self):
22
+ return str(self.args[0])
23
+
24
+ @property
25
+ def intent(self):
26
+ return self.args[1]
27
+
28
+ @property
29
+ def slot(self):
30
+ return self.args[2]
31
+
32
+ @property
33
+ def custom_data(self):
34
+ return self.args[3]
snips_skill/expr.py ADDED
@@ -0,0 +1,172 @@
1
+ import re
2
+ from collections import namedtuple
3
+
4
+ import ply.lex as lex
5
+ import ply.yacc as yacc
6
+
7
+
8
+ class Parser:
9
+ "Parse very simple expressions into callable boolean conditions"
10
+
11
+ class Expr(namedtuple("Expr", "keys expr")):
12
+ """
13
+ Container for a callable boolean predicate
14
+ along with the topic names being evaluated
15
+ """
16
+
17
+ last_state = None
18
+
19
+ def __call__(self, *args, **kw):
20
+ "Syntactic sugar"
21
+ self.last_state = self.expr(*args, **kw)
22
+ return self.last_state
23
+
24
+ def __hash__(self):
25
+ return hash(self.expr)
26
+
27
+ tokens = (
28
+ "AND",
29
+ "EQUAL",
30
+ "GREATER_EQUAL",
31
+ "GREATER",
32
+ "LESS_EQUAL",
33
+ "LESS",
34
+ "LPAREN",
35
+ "NOT_EQUAL",
36
+ "NOT",
37
+ "NUMBER",
38
+ "OR",
39
+ "REGEX_MATCH",
40
+ "RPAREN",
41
+ "STRING",
42
+ "TOPIC",
43
+ )
44
+
45
+ t_AND = r"and"
46
+ t_EQUAL = r"=="
47
+ t_GREATER = r">"
48
+ t_GREATER_EQUAL = r">="
49
+ t_LESS = r"<"
50
+ t_LESS_EQUAL = r"<="
51
+ t_LPAREN = r"\("
52
+ t_NOT = r"not"
53
+ t_NOT_EQUAL = r"!="
54
+ t_OR = r"or"
55
+ t_REGEX_MATCH = r"~="
56
+ t_RPAREN = r"\)"
57
+ t_TOPIC = r"[\w_-]+(/[\w_-]+)+"
58
+
59
+ # Ignored characters
60
+ t_ignore = " \t\r\n"
61
+
62
+ def t_NUMBER(self, t):
63
+ r"(\+|-)?\d+(\.\d+)?"
64
+ t.value = float(t.value)
65
+ return t
66
+
67
+ def t_STRING(self, t):
68
+ r"'[^']*'"
69
+ t.value = t.value[1:-1]
70
+ return t
71
+
72
+ # Track line numbers
73
+ def t_newline(self, t):
74
+ r"\n+"
75
+ t.lexer.lineno += len(t.value)
76
+
77
+ def t_error(self, t):
78
+ assert False, f"Illegal character '{t.value[0]}'"
79
+
80
+ # -#-# Grammar rules below #-#-#
81
+
82
+ precedence = (
83
+ ("left", "OR"),
84
+ ("left", "AND"),
85
+ ("right", "NOT"),
86
+ (
87
+ "left",
88
+ "GREATER",
89
+ "LESS",
90
+ "GREATER_EQUAL",
91
+ "LESS_EQUAL",
92
+ "EQUAL",
93
+ "NOT_EQUAL",
94
+ "REGEX_MATCH",
95
+ ),
96
+ )
97
+
98
+ def p_expr_term(self, p):
99
+ "expr : term"
100
+ p[0] = p[1]
101
+
102
+ def p_expr_and(self, p):
103
+ "expr : expr AND expr"
104
+ lhs, rhs = p[1], p[3]
105
+ p[0] = self.Expr(lhs.keys | rhs.keys, lambda s: lhs(s) and rhs(s))
106
+
107
+ def p_expr_or(self, p):
108
+ "expr : expr OR expr"
109
+ lhs, rhs = p[1], p[3]
110
+ p[0] = self.Expr(lhs.keys | rhs.keys, lambda s: lhs(s) or rhs(s))
111
+
112
+ def p_expr_not(self, p):
113
+ "expr : NOT expr"
114
+ cond = p[2]
115
+ p[0] = self.Expr(cond.keys, lambda s: not cond(s))
116
+
117
+ def p_expr_parenthesis(self, p):
118
+ "expr : LPAREN expr RPAREN"
119
+ p[0] = p[2]
120
+
121
+ def p_term_topic_less_number(self, p):
122
+ "term : TOPIC LESS NUMBER"
123
+ lhs, rhs = p[1], p[3]
124
+ p[0] = self.Expr(set([lhs]), lambda s: float(s[lhs]) < rhs)
125
+
126
+ def p_term_topic_less_equal_number(self, p):
127
+ "term : TOPIC LESS_EQUAL NUMBER"
128
+ lhs, rhs = p[1], p[3]
129
+ p[0] = self.Expr(set([lhs]), lambda s: float(s[lhs]) <= rhs)
130
+
131
+ def p_term_topic_greater_equal_number(self, p):
132
+ "term : TOPIC GREATER_EQUAL NUMBER"
133
+ lhs, rhs = p[1], p[3]
134
+ p[0] = self.Expr(set([lhs]), lambda s: float(s[lhs]) >= rhs)
135
+
136
+ def p_term_topic_greater_number(self, p):
137
+ "term : TOPIC GREATER NUMBER"
138
+ lhs, rhs = p[1], p[3]
139
+ p[0] = self.Expr(set([lhs]), lambda s: float(s[lhs]) > rhs)
140
+
141
+ def p_term_topic_equal_literal(self, p):
142
+ "term : TOPIC EQUAL literal"
143
+ lhs, rhs = p[1], p[3]
144
+ cast = type(rhs)
145
+ p[0] = self.Expr(set([lhs]), lambda s: cast(s[lhs]) == rhs)
146
+
147
+ def p_term_topic_not_equal_literal(self, p):
148
+ "term : TOPIC NOT_EQUAL literal"
149
+ lhs, rhs = p[1], p[3]
150
+ cast = type(rhs)
151
+ p[0] = self.Expr(set([lhs]), lambda s: cast(s[lhs]) != rhs)
152
+
153
+ def p_term_topic_regex_match_string(self, p):
154
+ "term : TOPIC REGEX_MATCH STRING"
155
+ lhs, rhs = p[1], p[3]
156
+ pattern = re.compile(rhs)
157
+ p[0] = self.Expr(set([lhs]), lambda s: pattern.search(str(s[lhs])) is not None)
158
+
159
+ def p_literal(self, p):
160
+ """literal : NUMBER
161
+ | STRING"""
162
+ p[0] = p[1]
163
+
164
+ def p_error(self, p):
165
+ assert False, "Syntax error at: %s" % (p or "EOF")
166
+
167
+ def __init__(self, **kwargs):
168
+ self.lexer = lex.lex(module=self)
169
+ self.parser = yacc.yacc(module=self, **kwargs)
170
+
171
+ def parse(self, text: str, **kwargs):
172
+ return self.parser.parse(text, lexer=self.lexer, **kwargs)
snips_skill/i18n.py ADDED
@@ -0,0 +1,83 @@
1
+ import gettext
2
+ import locale
3
+ import os
4
+ from collections import namedtuple
5
+
6
+ __all__ = (
7
+ "_",
8
+ "ALL_ROOMS",
9
+ "CONFIRMATIONS",
10
+ "DEFAULT_ROOM_NAMES",
11
+ "ROOMS",
12
+ "get_translations",
13
+ "ngettext",
14
+ "room_with_article",
15
+ "room_with_preposition",
16
+ )
17
+
18
+
19
+ def get_translations(path: str, domain: str = "messages"):
20
+ "Install translations"
21
+ language, encoding = locale.getlocale()
22
+ assert type(language) is str, "Unsupported locale: %s" % locale.getlocale()
23
+ locale.setlocale(locale.LC_ALL, (language, encoding))
24
+ translation = gettext.translation(
25
+ domain,
26
+ localedir=os.path.join(os.path.dirname(path), "locale"),
27
+ languages=[language],
28
+ fallback=True,
29
+ )
30
+ return translation.gettext, translation.ngettext
31
+
32
+
33
+ _, ngettext = get_translations(__file__, "snips_skill")
34
+
35
+ RoomName = namedtuple("RoomName", "with_article, with_preposition")
36
+
37
+ CONFIRMATIONS = (
38
+ _("done"),
39
+ _("here you are"),
40
+ _("OK"),
41
+ _("yes"),
42
+ _("finished"),
43
+ _("alright"),
44
+ _("at your service"),
45
+ )
46
+
47
+ DEFAULT_ROOM_NAMES = (
48
+ _("here"),
49
+ _("this room"),
50
+ )
51
+
52
+ ALL_ROOMS = (
53
+ _("all"),
54
+ _("everywhere"),
55
+ _("everything"),
56
+ )
57
+
58
+ ROOMS = {
59
+ # Map translated room names to room names with articles and prepositions
60
+ # for languages that use genders for room names, e.g. German
61
+ _("bathroom").lower(): RoomName(_("the bathroom"), _("in the bathroom")),
62
+ _("bedroom").lower(): RoomName(_("the bedroom"), _("in the bedroom")),
63
+ _("dining room").lower(): RoomName(_("the dining room"), _("in the dining room")),
64
+ _("livingroom").lower(): RoomName(_("the livingroom"), _("in the livingroom")),
65
+ _("kid's room").lower(): RoomName(_("the kid's room"), _("in the kid's room")),
66
+ _("kitchen").lower(): RoomName(_("the kitchen"), _("in the kitchen")),
67
+ _("office").lower(): RoomName(_("the office"), _("in the office")),
68
+ _("hall").lower(): RoomName(_("the hall"), _("in the hall")),
69
+ _("garden").lower(): RoomName(_("the garden"), _("in the garden")),
70
+ _("unknown room").lower(): RoomName(_("an unknown room"), _("in an unknown room")),
71
+ }
72
+
73
+
74
+ def room_with_article(room_name: str) -> str:
75
+ "Get the spoken room name with the definite article"
76
+ room = ROOMS.get(room_name.lower())
77
+ return room.with_article if room else _("the {room}").format(room=room_name)
78
+
79
+
80
+ def room_with_preposition(room_name: str) -> str:
81
+ 'Get the spoken room name with "in" preposition'
82
+ room = ROOMS.get(room_name.lower())
83
+ return room.with_preposition if room else _("in the {room}").format(room=room_name)
snips_skill/intent.py ADDED
@@ -0,0 +1,136 @@
1
+ from datetime import datetime, timedelta
2
+ from functools import cached_property
3
+ from typing import Annotated, Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+ from pydantic.alias_generators import to_camel
7
+
8
+ __all__ = ("IntentPayload",)
9
+
10
+
11
+ class UnknownValue(BaseModel):
12
+ kind: Literal["Unknown"]
13
+ value: str
14
+
15
+
16
+ class NumericValue(BaseModel):
17
+ kind: Literal["Number"]
18
+ value: int | float
19
+
20
+
21
+ # See: https://docs.snips.ai/articles/platform/dialog/slot-types#time-related-entities
22
+ class InstantTimeValue(BaseModel):
23
+ kind: Literal["InstantTime"]
24
+ grain: str
25
+ precision: str
26
+ value: datetime
27
+
28
+
29
+ class TimeIntervalValue(BaseModel):
30
+ kind: Literal["TimeInterval"]
31
+ t1: datetime | None = Field(alias="from", default=None)
32
+ t2: datetime | None = Field(alias="to", default=None)
33
+
34
+
35
+ # See: https://docs.snips.ai/articles/platform/dialog/slot-types#duration-entities
36
+ class DurationValue(BaseModel):
37
+ kind: Literal["Duration"]
38
+ precision: str
39
+ years: int = 0
40
+ quarters: int = 0
41
+ months: int = 0
42
+ weeks: int = 0
43
+ days: int = 0
44
+ hours: int = 0
45
+ minutes: int = 0
46
+ seconds: int = 0
47
+
48
+ def as_timedelta(self) -> timedelta:
49
+ # TODO: years, quarters and months are not parsed
50
+ return timedelta(
51
+ weeks=self.weeks,
52
+ days=self.days,
53
+ hours=self.hours,
54
+ minutes=self.minutes,
55
+ seconds=self.seconds,
56
+ )
57
+
58
+
59
+ class TemperatureValue(BaseModel):
60
+ kind: Literal["Temperature"]
61
+ unit: str
62
+ value: float
63
+
64
+
65
+ class MonetaryValue(BaseModel):
66
+ kind: Literal["AmountOfMoney"]
67
+ unit: str
68
+ precision: str
69
+ value: float
70
+
71
+
72
+ class Range(BaseModel, alias_generator=to_camel, serialize_by_alias=True):
73
+ start: int
74
+ end: int
75
+ raw_start: int
76
+ raw_end: int
77
+
78
+
79
+ SlotValue = Annotated[
80
+ UnknownValue
81
+ | NumericValue
82
+ | InstantTimeValue
83
+ | TimeIntervalValue
84
+ | DurationValue
85
+ | TemperatureValue
86
+ | MonetaryValue,
87
+ Field(discriminator="kind"),
88
+ ]
89
+
90
+
91
+ # See: https://docs.snips.ai/reference/dialogue#slot
92
+ class Slot(BaseModel, alias_generator=to_camel, serialize_by_alias=True):
93
+ entity: str
94
+ slot_name: str
95
+ raw_value: str
96
+ confidence: float
97
+ range: Range | None
98
+ value: SlotValue
99
+
100
+
101
+ # See: https://docs.snips.ai/reference/dialogue#intent
102
+ class Intent(BaseModel, alias_generator=to_camel, serialize_by_alias=True):
103
+ intent_name: str
104
+ confidence_score: float
105
+
106
+
107
+ class AsrToken(BaseModel, alias_generator=to_camel, serialize_by_alias=True):
108
+ value: str
109
+ confidence: float
110
+ range_start: int
111
+ range_end: int
112
+ time: None
113
+
114
+
115
+ # See: https://docs.snips.ai/reference/dialogue#intent
116
+ class IntentPayload(BaseModel, alias_generator=to_camel, serialize_by_alias=True):
117
+ session_id: str
118
+ input: str
119
+ intent: Intent
120
+ site_id: str
121
+ slots: list[Slot] | None = None
122
+ id: str | None = None
123
+ session_id: str
124
+ custom_data: str
125
+ asr_tokens: list[list[AsrToken]]
126
+ asr_confidence: float
127
+ raw_input: str
128
+ # wakeword_id: str
129
+ # lang: None
130
+
131
+ @cached_property
132
+ def slot_values(self) -> dict[str, SlotValue]:
133
+ return {s.slot_name: s.value for s in self.slots or []}
134
+
135
+ def __getitem__(self, name):
136
+ return self.slot_values[name]