Skip to content

Commit

Permalink
refactor/adapt_ContextManager (mycroft-core/pull/2886)
Browse files Browse the repository at this point in the history
cherry pick of MycroftAI#2886

Timed Context Manager

Makes the Mycroft-core Adapt Context Manager depend on the
ContextManager in Adapt. This removes a bunch of duplicated code. The
current state of the class reflects _only_ the modifications done to
integrate the context with the Mycroft intent system.

The key differences from the default Adapt context manager are
- Frames are timedout after a set time
- get_context will only return the latest match
- methods for removing context (all and specific)
  • Loading branch information
forslund authored and JarbasAl committed Jul 9, 2021
1 parent 5dcc06d commit da0b916
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 79 deletions.
4 changes: 2 additions & 2 deletions mycroft/skills/intent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ def handle_add_context(self, message):
entity['match'] = word
entity['key'] = word
entity['origin'] = origin
self.adapt_service.context_manager.inject_context(entity)
self.adapt_service.add_context(entity)

def handle_remove_context(self, message):
"""Remove specific context
Expand All @@ -499,7 +499,7 @@ def handle_remove_context(self, message):

def handle_clear_context(self, _):
"""Clears all keywords from context """
self.adapt_service.context_manager.clear_context()
self.adapt_service.reset_context()

def handle_get_intent(self, message):
"""Get intent from either adapt or padatious.
Expand Down
117 changes: 40 additions & 77 deletions mycroft/skills/intent_services/adapt_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from threading import Lock
import time

from adapt.context import ContextManagerFrame
from adapt.context import ContextManager as AdaptContextManager
from adapt.engine import IntentDeterminationEngine
from adapt.intent import IntentBuilder

Expand Down Expand Up @@ -65,104 +65,61 @@ def _strip_result(context_features):
return stripped


class ContextManager:
def _frame_timedout(frame, timeout):
"""Check if a frame has timed out using it's metadata.
frame (ContextManagerFrame):
"""
current_time = time.monotonic()
return current_time > frame.metadata['timestamp'] + timeout


class ContextManager(AdaptContextManager):
"""Adapt Context Manager
Use to track context throughout the course of a conversational session.
How to manage a session's lifecycle is not captured here.
This class extends the default ContextManager in adapt to "timeout"
context frames after a set time. it also limits the context returned
to a single entry.
"""
def __init__(self, timeout):
self.frame_stack = []
super().__init__()
self.timeout = timeout * 60 # minutes to seconds

def clear_context(self):
"""Remove all contexts."""
self.frame_stack = []

def remove_context(self, context_id):
"""Remove a specific context entry.
Args:
context_id (str): context entry to remove
"""
self.frame_stack = [(f, t) for (f, t) in self.frame_stack
if context_id in f.entities[0].get('data', [])]
self.frame_stack = [frame for frame in self.frame_stack
if context_id in frame.entities[0].get('data', [])]

def inject_context(self, entity, metadata=None):
"""
Args:
entity(object): Format example...
{'data': 'Entity tag as <str>',
'key': 'entity proper name as <str>',
'confidence': <float>'
}
metadata(object): dict, arbitrary metadata about entity injected
"""
"""Inject context and add timestamp to metadata if missing."""
metadata = metadata or {}
try:
if self.frame_stack:
top_frame = self.frame_stack[0]
else:
top_frame = None
if top_frame and top_frame[0].metadata_matches(metadata):
top_frame[0].merge_context(entity, metadata)
else:
frame = ContextManagerFrame(entities=[entity],
metadata=metadata.copy())
self.frame_stack.insert(0, (frame, time.time()))
except (IndexError, KeyError):
pass
if 'timestamp' not in metadata:
metadata['timestamp'] = time.monotonic()
super().inject_context(entity, metadata)

def get_context(self, max_frames=None, missing_entities=None):
""" Constructs a list of entities from the context.
def get_context(self, max_frames=None, missing_entities=[]):
"""Extends the get_context from Adapt's ContextManager.
Args:
Extensions on top of parent class:
- Timeout Context frames after a set time
- Only return the most recent matching entity
Arguments:
max_frames(int): maximum number of frames to look back
missing_entities(list of str): a list or set of tag names,
as strings
Returns:
list: a list of entities
"""
missing_entities = missing_entities or []

relevant_frames = [frame[0] for frame in self.frame_stack if
time.time() - frame[1] < self.timeout]
if not max_frames or max_frames > len(relevant_frames):
max_frames = len(relevant_frames)

missing_entities = list(missing_entities)
context = []
last = ''
depth = 0
entity = {}
for i in range(max_frames):
frame_entities = [entity.copy() for entity in
relevant_frames[i].entities]
for entity in frame_entities:
entity['confidence'] = entity.get('confidence', 1.0) \
/ (2.0 + depth)
context += frame_entities

# Update depth
if entity['origin'] != last or entity['origin'] == '':
depth += 1
last = entity['origin']

result = []
if missing_entities:
for entity in context:
if entity.get('data') in missing_entities:
result.append(entity)
# NOTE: this implies that we will only ever get one
# of an entity kind from context, unless specified
# multiple times in missing_entities. Cannot get
# an arbitrary number of an entity kind.
missing_entities.remove(entity.get('data'))
else:
result = context

# Only use the latest keyword
self.frame_stack = [frame for frame in self.frame_stack
if not _frame_timedout(frame, self.timeout)]
result = super().get_context(max_frames, missing_entities)
# Only use the latest keyword
return _strip_result(result)


Expand All @@ -171,13 +128,18 @@ class AdaptService:
def __init__(self, config):
self.config = config
self.engine = IntentDeterminationEngine()
self.lock = Lock()
# Context related intializations
self.context_keywords = self.config.get('keywords', [])
self.context_max_frames = self.config.get('max_frames', 3)
self.context_timeout = self.config.get('timeout', 2)
self.context_greedy = self.config.get('greedy', False)
self.context_manager = None
self.reset_context()

def reset_context(self):
"""Reset the context manager used by the service."""
self.context_manager = ContextManager(self.context_timeout)
self.lock = Lock()

def update_context(self, intent):
"""Updates context with keyword from the intent.
Expand Down Expand Up @@ -208,6 +170,7 @@ def match_intent(self, utterances, _=None, __=None):
Returns:
Intent structure, or None if no match was found.
"""
adapt_context = self.context_manager
best_intent = {}

def take_best(intent, utt):
Expand All @@ -225,7 +188,7 @@ def take_best(intent, utt):
intents = [i for i in self.engine.determine_intent(
utt, 100,
include_tags=True,
context_manager=self.context_manager)]
context_manager=adapt_context)]
if intents:
take_best(intents[0], utt_tup[0])

Expand Down

0 comments on commit da0b916

Please sign in to comment.