feat: plan deterministic generated conversations

This commit is contained in:
Rijad Zuzo
2026-08-12 20:32:53 +02:00
parent 4b58c534d0
commit 41b37a4962
28 changed files with 1694 additions and 0 deletions
@@ -0,0 +1,203 @@
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 ""