204 lines
6.8 KiB
GDScript
204 lines
6.8 KiB
GDScript
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 ""
|