def with_cycles_removed(self): # type: () -> StoryGraph """Create a graph with the cyclic edges removed from this graph.""" story_end_checkpoints = self.story_end_checkpoints.copy() cyclic_edges = self.cyclic_edges() # we need to remove the start steps and replace them with steps ending # in a special end checkpoint steps_to_be_removed = {start.id for start, _ in cyclic_edges} story_steps = [ s for s in self.story_steps if s.id not in steps_to_be_removed ] # add changed start steps again for s, e in cyclic_edges: cid = utils.generate_id() start_cid = "CYCLE_S_" + cid end_cid = "CYCLE_E_" + cid story_end_checkpoints[start_cid] = end_cid modified_start = s.create_copy(use_new_id=True) modified_start.end_checkpoint = Checkpoint(start_cid) story_steps.append(modified_start) modified_end = e.create_copy(use_new_id=True) modified_end.start_checkpoint = Checkpoint(end_cid) story_steps.append(modified_end) return StoryGraph(story_steps, story_end_checkpoints)
def add_user_messages(self, messages): self.ensure_current_steps() if len(messages) == 1: # If there is only one possible intent, we'll keep things simple for t in self.current_steps: t.add_user_message(messages[0]) else: # If there are multiple different intents the # user can use the express the same thing # we need to copy the blocks and create one # copy for each possible message generated_checkpoint = utils.generate_id("GENERATED_M_") updated_steps = [] for t in self.current_steps: for m in messages: copied = t.create_copy(use_new_id=True) copied.add_user_message(m) copied.end_checkpoint = Checkpoint(generated_checkpoint) updated_steps.append(copied) self.current_steps = updated_steps
def add_user_messages(self, messages): self.ensure_current_steps() if len(messages) == 1: # If there is only one possible intent, we'll keep things simple for t in self.current_steps: t.add_user_message(messages[0]) else: # If there are multiple different intents the # user can use the express the same thing # we need to copy the blocks and create one # copy for each possible message prefix = GENERATED_CHECKPOINT_PREFIX + "OR_" generated_checkpoint = utils.generate_id(prefix, GENERATED_HASH_LENGTH) updated_steps = [] for t in self.current_steps: for m in messages: copied = t.create_copy(use_new_id=True) copied.add_user_message(m) copied.end_checkpoints = [Checkpoint(generated_checkpoint)] updated_steps.append(copied) self.current_steps = updated_steps
def with_cycles_removed(self) -> 'StoryGraph': """Create a graph with the cyclic edges removed from this graph.""" story_end_checkpoints = self.story_end_checkpoints.copy() cyclic_edge_ids = self.cyclic_edge_ids # we need to remove the start steps and replace them with steps ending # in a special end checkpoint story_steps = {s.id: s for s in self.story_steps} # collect all overlapping checkpoints # we will remove unused start ones all_overlapping_cps = set() if self.cyclic_edge_ids: # we are going to do this in a recursive way. we are going to # remove one cycle and then we are going to # let the cycle detection run again # this is not inherently necessary so if this becomes a performance # issue, we can change it. It is actually enough to run the cycle # detection only once and then remove one cycle after another, but # since removing the cycle is done by adding / removing edges and # nodes # the logic is a lot easier if we only need to make sure the # change is consistent if we only change one compared to # changing all of them. for s, e in cyclic_edge_ids: cid = utils.generate_id(max_chars=GENERATED_HASH_LENGTH) prefix = GENERATED_CHECKPOINT_PREFIX + CHECKPOINT_CYCLE_PREFIX # need abbreviations otherwise they are not visualized well sink_cp_name = prefix + "SINK_" + cid connector_cp_name = prefix + "CONN_" + cid source_cp_name = prefix + "SRC_" + cid story_end_checkpoints[sink_cp_name] = source_cp_name overlapping_cps = self.overlapping_checkpoint_names( story_steps[s].end_checkpoints, story_steps[e].start_checkpoints) all_overlapping_cps.update(overlapping_cps) # change end checkpoints of starts start = story_steps[s].create_copy(use_new_id=False) start.end_checkpoints = [ cp for cp in start.end_checkpoints if cp.name not in overlapping_cps ] start.end_checkpoints.append(Checkpoint(sink_cp_name)) story_steps[s] = start needs_connector = False for k, step in list(story_steps.items()): additional_ends = [] for original_cp in overlapping_cps: for cp in step.start_checkpoints: if cp.name == original_cp: if k == e: cp_name = source_cp_name else: cp_name = connector_cp_name needs_connector = True if not self._is_checkpoint_in_list( cp_name, cp.conditions, step.start_checkpoints): # add checkpoint only if it was not added additional_ends.append( Checkpoint(cp_name, cp.conditions)) if additional_ends: updated = step.create_copy(use_new_id=False) updated.start_checkpoints.extend(additional_ends) story_steps[k] = updated if needs_connector: start.end_checkpoints.append(Checkpoint(connector_cp_name)) # the process above may generate unused checkpoints # we need to find them and remove them self._remove_unused_generated_cps(story_steps, all_overlapping_cps, story_end_checkpoints) return StoryGraph(list(story_steps.values()), story_end_checkpoints)
def with_cycles_removed(self): # type: () -> StoryGraph """Create a graph with the cyclic edges removed from this graph.""" if not self.cyclic_edge_ids: return self story_end_checkpoints = self.story_end_checkpoints.copy() cyclic_edge_ids = self.cyclic_edge_ids # we need to remove the start steps and replace them with steps ending # in a special end checkpoint story_steps = {s.id: s for s in self.story_steps} # we are going to do this in a recursive way. we are going to remove # one cycle and then we are going to let the cycle detection run again # this is not inherently necessary so if this becomes a performance # issue, we can change it. It is actually enough to run the cycle # detection only once and then remove one cycle after another, but # since removing the cycle is done by adding / removing edges and nodes # the logic is a lot easier if we only need to make sure the change is # consistent if we only change one compared to changing all of them. for s, e in cyclic_edge_ids: cid = utils.generate_id(max_chars=GENERATED_HASH_LENGTH) sink_cid = GENERATED_CHECKPOINT_PREFIX + "SINK_" + cid connector_cid = GENERATED_CHECKPOINT_PREFIX + "CONNECT_" + cid source_cid = GENERATED_CHECKPOINT_PREFIX + "SOURCE_" + cid story_end_checkpoints[sink_cid] = source_cid overlapping_cps = self.overlapping_checkpoint_names( story_steps[s].end_checkpoints, story_steps[e].start_checkpoints) # changed all starts start = story_steps[s].create_copy(use_new_id=False) start.end_checkpoints = [ cp for cp in start.end_checkpoints if cp.name not in overlapping_cps ] start.end_checkpoints.append(Checkpoint(sink_cid)) story_steps[s] = start needs_connector = False for k, step in list(story_steps.items()): additional_ends = [] for original_cp in overlapping_cps: for cp in step.start_checkpoints: if cp.name == original_cp: if k == e: cid = source_cid else: cid = connector_cid needs_connector = True additional_ends.append( Checkpoint(cid, cp.conditions)) if additional_ends: updated = step.create_copy(use_new_id=False) updated.start_checkpoints.extend(additional_ends) story_steps[k] = updated if needs_connector: start.end_checkpoints.append(Checkpoint(connector_cid)) return StoryGraph(story_steps.values(), story_end_checkpoints)
def with_cycles_removed(self): # type: () -> StoryGraph """Create a graph with the cyclic edges removed from this graph.""" story_end_checkpoints = self.story_end_checkpoints.copy() cyclic_edge_ids = self.cyclic_edge_ids # we need to remove the start steps and replace them with steps ending # in a special end checkpoint story_steps = {s.id: s for s in self.story_steps} # collect all overlapping checkpoints # we will remove unused start ones all_overlapping_cps = set() if self.cyclic_edge_ids: # we are going to do this in a recursive way. we are going to remove # one cycle and then we are going to let the cycle detection run again # this is not inherently necessary so if this becomes a performance # issue, we can change it. It is actually enough to run the cycle # detection only once and then remove one cycle after another, but # since removing the cycle is done by adding / removing edges and nodes # the logic is a lot easier if we only need to make sure the change is # consistent if we only change one compared to changing all of them. for s, e in cyclic_edge_ids: cid = utils.generate_id(max_chars=GENERATED_HASH_LENGTH) prefix = GENERATED_CHECKPOINT_PREFIX + CHECKPOINT_CYCLE_PREFIX # need abbreviations otherwise they are not visualized well sink_cp_name = prefix + "SINK_" + cid connector_cp_name = prefix + "CONN_" + cid source_cp_name = prefix + "SRC_" + cid story_end_checkpoints[sink_cp_name] = source_cp_name overlapping_cps = self.overlapping_checkpoint_names( story_steps[s].end_checkpoints, story_steps[e].start_checkpoints) all_overlapping_cps.update(overlapping_cps) # change end checkpoints of starts start = story_steps[s].create_copy(use_new_id=False) start.end_checkpoints = [cp for cp in start.end_checkpoints if cp.name not in overlapping_cps] start.end_checkpoints.append(Checkpoint(sink_cp_name)) story_steps[s] = start needs_connector = False for k, step in list(story_steps.items()): additional_ends = [] for original_cp in overlapping_cps: for cp in step.start_checkpoints: if cp.name == original_cp: if k == e: cp_name = source_cp_name else: cp_name = connector_cp_name needs_connector = True if not self._is_checkpoint_in_list( cp_name, cp.conditions, step.start_checkpoints): # add checkpoint only if it was not added additional_ends.append( Checkpoint(cp_name, cp.conditions)) if additional_ends: updated = step.create_copy(use_new_id=False) updated.start_checkpoints.extend(additional_ends) story_steps[k] = updated if needs_connector: start.end_checkpoints.append(Checkpoint(connector_cp_name)) # the process above may generate unused checkpoints # we need to find them and remove them self._remove_unused_generated_cps(story_steps, all_overlapping_cps, story_end_checkpoints) return StoryGraph(list(story_steps.values()), story_end_checkpoints)