class_name DialogueManagerConversationPresenter extends ConversationPresenter const DEFAULT_TEMPLATE_CATALOG_PATH := "res://simulation/dialogue/resources/jajce_conversation_templates.tres" var last_error := "" var _dialogue_manager: Object var _templates: ConversationTemplateCatalog func _init( dialogue_manager: Object = null, template_catalog: ConversationTemplateCatalog = null ) -> void: _dialogue_manager = dialogue_manager _templates = template_catalog if _templates == null: _templates = load(DEFAULT_TEMPLATE_CATALOG_PATH) as ConversationTemplateCatalog func present_turn(turn: ConversationTurn, presentation_context: Dictionary = {}) -> Variant: last_error = "" if turn == null or not turn.is_valid(): return _fail("Conversation turn is invalid") if not _presentation_context_is_valid(presentation_context): return _fail("Presentation context is invalid") var manager := _resolve_dialogue_manager() if manager == null or not manager.has_method("create_resource_from_text"): return _fail("Dialogue Manager v3 runtime is unavailable") if _templates == null or not _templates.validate().is_empty(): return _fail("Conversation template catalog is invalid") var source := build_source(turn, presentation_context) if source.is_empty(): return null var resource: Variant = manager.call("create_resource_from_text", source) if resource is Resource: resource.set_meta("conversation_id", String(turn.get_conversation_id())) resource.set_meta("conversation_revision", turn.get_revision()) resource.set_meta("semantic_act_id", String(turn.get_act().get_act_id())) resource.set_meta("semantic_intent_id", String(turn.get_act().get_intent_id())) return resource func build_source(turn: ConversationTurn, presentation_context: Dictionary = {}) -> String: last_error = "" if turn == null or not turn.is_valid(): return _fail_string("Conversation turn is invalid") if not _presentation_context_is_valid(presentation_context): return _fail_string("Presentation context is invalid") if _templates == null or not _templates.validate().is_empty(): return _fail_string("Conversation template catalog is invalid") var act := turn.get_act() var speaker_label := _entity_name(act.get_speaker(), presentation_context) var listener_label := _entity_name(act.get_listener(), presentation_context) var topic_label := _topic_name(act.get_causal_topic_ids(), presentation_context) var speaker_name := sanitize_dialogue_text(speaker_label) var line_text := _render_template( _select_template( _templates.get_line_templates(act.get_intent_id()), _stable_key(turn, act.get_act_id(), act.get_intent_id(), &"line") ), speaker_label, listener_label, topic_label ) var line_tags := PackedStringArray( [ "conversation_id=%s" % turn.get_conversation_id(), "revision=%d" % turn.get_revision(), "act_id=%s" % act.get_act_id(), "intent_id=%s" % act.get_intent_id(), ] ) for topic_id in act.get_causal_topic_ids(): line_tags.append("topic_id=%s" % topic_id) var lines := PackedStringArray( [ "~ start", "%s: %s [#%s]" % [speaker_name, line_text, ",".join(line_tags)], ] ) for option in turn.get_options(): if not option.is_enabled(): continue var option_topic := _topic_name(option.get_causal_topic_ids(), presentation_context) var option_text := _render_template( _select_template( _templates.get_option_templates(option.get_intent_id()), _stable_key(turn, option.get_option_id(), option.get_intent_id(), &"option") ), _entity_name(option.get_speaker(), presentation_context), _entity_name(option.get_listener(), presentation_context), option_topic ) var option_tags := PackedStringArray( [ "option_id=%s" % option.get_option_id(), "intent_id=%s" % option.get_intent_id(), "enabled=true", ] ) for topic_id in option.get_causal_topic_ids(): option_tags.append("topic_id=%s" % topic_id) lines.append("- %s [#%s] => END" % [option_text, ",".join(option_tags)]) lines.append("=> END") return "\n".join(lines) static func sanitize_dialogue_text(value: String) -> String: var sanitized := value.replace("\\", "\") sanitized = sanitized.replace("\r", " ").replace("\n", " ").replace("\t", " ") sanitized = sanitized.replace("[", "[").replace("]", "]") sanitized = sanitized.replace("{", "{").replace("}", "}") sanitized = sanitized.replace("=>", "=>") sanitized = sanitized.replace(":", "\\:") return " ".join(sanitized.split(" ", false)).strip_edges() func _stable_key( turn: ConversationTurn, semantic_id: StringName, intent_id: StringName, kind: StringName ) -> String: return ( "%s|%d|%s|%s|%s" % [turn.get_conversation_id(), turn.get_revision(), semantic_id, intent_id, kind] ) func _select_template(templates: Array[String], stable_key: String) -> String: if templates.is_empty(): return "..." var digest := stable_key.sha256_text() var stable_index := 0 for index in range(16): stable_index = (stable_index * 33 + digest.unicode_at(index)) % templates.size() return templates[stable_index] func _render_template( template: String, speaker_name: String, listener_name: String, topic_name: String ) -> String: return sanitize_dialogue_text( template.replace("{speaker}", speaker_name).replace("{listener}", listener_name).replace( "{topic}", topic_name ) ) func _entity_name(entity: WorldEntityRef, context: Dictionary) -> String: var names: Dictionary = context.get("entity_names", {}) var value := String( names.get(entity.index_key(), names.get(String(entity.get_entity_id()), "")) ) if value.is_empty(): value = String(entity.get_entity_id()).replace("_", " ").capitalize() return value func _topic_name(topic_ids: Array[StringName], context: Dictionary) -> String: if topic_ids.is_empty(): return "the matter at hand" var labels: Dictionary = context.get("topic_labels", {}) var topic_id := topic_ids[0] var value := String(labels.get(String(topic_id), "")) if value.is_empty(): value = String(topic_id).replace("_", " ") return value func _presentation_context_is_valid(context: Dictionary) -> bool: for key in context: if ( (key is not String and key is not StringName) or String(key) not in ["entity_names", "topic_labels"] or context[key] is not Dictionary ): return false for entry_key in context[key] as Dictionary: if entry_key is not String and entry_key is not StringName: return false var value: Variant = context[key][entry_key] if value is not String: return false return true func _resolve_dialogue_manager() -> Object: if _dialogue_manager != null and is_instance_valid(_dialogue_manager): return _dialogue_manager if Engine.has_singleton("DialogueManager"): return Engine.get_singleton("DialogueManager") return null func _fail(message: String) -> Variant: last_error = message return null func _fail_string(message: String) -> String: last_error = message return ""