From 31419452491f50b63dc7e2bc0b1a46b1cd50e314 Mon Sep 17 00:00:00 2001 From: vegu-ai-tools <152010387+vegu-ai-tools@users.noreply.github.com> Date: Sun, 17 May 2026 14:25:11 +0300 Subject: [PATCH] move scene message nodes to separate module --- .../game/engine/nodes/load_definitions.py | 1 + src/talemate/game/engine/nodes/scene.py | 327 ----------------- .../game/engine/nodes/scene_messages.py | 340 ++++++++++++++++++ tests/test_nodes_scene.py | 12 +- 4 files changed, 348 insertions(+), 332 deletions(-) create mode 100644 src/talemate/game/engine/nodes/scene_messages.py diff --git a/src/talemate/game/engine/nodes/load_definitions.py b/src/talemate/game/engine/nodes/load_definitions.py index 577cb814..0701d31c 100644 --- a/src/talemate/game/engine/nodes/load_definitions.py +++ b/src/talemate/game/engine/nodes/load_definitions.py @@ -3,6 +3,7 @@ import talemate.game.engine.nodes.command # noqa: F401 import talemate.game.engine.nodes.logic # noqa: F401 import talemate.game.engine.nodes.state # noqa: F401 import talemate.game.engine.nodes.scene # noqa: F401 +import talemate.game.engine.nodes.scene_messages # noqa: F401 import talemate.game.engine.nodes.scene_intent # noqa: F401 import talemate.game.engine.nodes.world_state # noqa: F401 import talemate.game.engine.nodes.run # noqa: F401 diff --git a/src/talemate/game/engine/nodes/scene.py b/src/talemate/game/engine/nodes/scene.py index 98836cf7..be2c374f 100644 --- a/src/talemate/game/engine/nodes/scene.py +++ b/src/talemate/game/engine/nodes/scene.py @@ -777,333 +777,6 @@ class UnpackInteractionState(Node): ) -@register("scene/message/CharacterMessage") -class CharacterMessage(Node): - """ - Creates a character message from a character and a message - - Inputs: - - - character: The character object - - message: The message to send - - source: The source of the message - player or ai, so whether the message is result of user input or AI generated - - from_choice: For player messages this indicates that the message was generated from a choice selection, for ai sourced messages this indicates the instruction that was followed - - Properties: - - - source: The source of the message - - Outputs: - - - message: The message object (this is a scene_message.CharacterMessage instance) - """ - - class Fields: - source = PropertyField( - name="source", - description="The source of the message", - type="str", - default="player", - choices=[ - "player", - "ai", - ], - ) - - def __init__(self, title="Character Message", **kwargs): - super().__init__(title=title, **kwargs) - - def setup(self): - self.add_input("character", socket_type="character") - self.add_input("message", socket_type="str") - self.add_input("source", socket_type="str", optional=True) - self.add_input("from_choice", socket_type="str", optional=True) - - self.set_property("source", "player") - - self.add_output("message", socket_type="message_object") - - async def run(self, state: GraphState): - character: "Character" = self.get_input_value("character") - message = self.get_input_value("message") - source = self.get_input_value("source") - from_choice = self.get_input_value("from_choice") - - extra = {} - - if isinstance(from_choice, str): - extra["from_choice"] = from_choice - - # Capture the character's current avatar at message creation time - # (not default avatar - messages only use current_avatar if set) - if character.current_avatar: - extra["asset_id"] = character.current_avatar - extra["asset_type"] = "avatar" - - # prefix name: if not already prefixed - if not message.startswith(f"{character.name}: "): - message = f"{character.name}: {message}" - - message = scene_message.CharacterMessage(message, source=source, **extra) - - self.set_output_values({"message": message}) - - -@register("scene/message/NarratorMessage") -class NarratorMessage(Node): - """ - Creates a narrator message - - Inputs: - - - message: The message to send - - source: The source of the message - player or ai, so whether the message is result of user input or AI generated - - meta: A dictionary of meta information to attach to the message. This will generally be arguments and function name that was called on the narrator agent to generate the message and will be used when regenerating the message. - - Properties: - - - source: The source of the message - - Outputs: - - - message: The message object (this is a scene_message.NarratorMessage instance) - """ - - class Fields: - source = PropertyField( - name="source", - description="The source of the message", - type="str", - default="ai", - choices=[ - "player", - "ai", - ], - ) - - def __init__(self, title="Narrator Message", **kwargs): - super().__init__(title=title, **kwargs) - - def setup(self): - self.add_input("message", socket_type="str") - self.add_input("source", socket_type="str", optional=True) - self.add_input("meta", socket_type="dict", optional=True) - - self.set_property("source", "ai") - - self.add_output("message", socket_type="message_object") - - async def run(self, state: GraphState): - message = self.get_input_value("message") - source = self.get_input_value("source") - meta = self.get_input_value("meta") - - extra = {} - - if meta and isinstance(meta, dict): - extra["meta"] = meta - - message = scene_message.NarratorMessage(message, source=source, **extra) - - self.set_output_values({"message": message}) - - -@register("scene/message/DirectorMessage") -class DirectorMessage(Node): - """ - Creates a director message - - Inputs: - - - message: The message to send - - source: The source of the message - player or ai, so whether the message is result of user input or AI generated - - meta: A dictionary of meta information to attach to the message. Can hold the character name that the message is related to. - - character: The character object that the message is related to - - Properties: - - - source: The source of the message - - action: Describes the director action - - Outputs: - - - message: The message object (this is a scene_message.DirectorMessage instance) - """ - - class Fields: - source = PropertyField( - name="source", - description="The source of the message", - type="str", - default="ai", - choices=[ - "player", - "ai", - ], - ) - - action = PropertyField( - name="action", - description="Describes the director action", - type="str", - default="actor_instruction", - choices=[ - "actor_instruction", - "user_direction", - ], - ) - - subtype = PropertyField( - name="subtype", - description="The subtype of the director message, used for further categorization of the message", - type="str", - default=UNRESOLVED, - choices=[ - "function_call", - "user_direction", - ], - ) - - def __init__(self, title="Director Message", **kwargs): - super().__init__(title=title, **kwargs) - - def setup(self): - self.add_input("message", socket_type="str") - self.add_input("source", socket_type="str", optional=True) - self.add_input("meta", socket_type="dict", optional=True) - self.add_input("character", socket_type="character", optional=True) - self.add_input("action", socket_type="str", optional=True) - self.add_input("subtype", socket_type="str", optional=True) - - self.set_property("source", "ai") - self.set_property("action", "actor_instruction") - self.set_property("subtype", UNRESOLVED) - - self.add_output("message", socket_type="message_object") - self.add_output("source", socket_type="str") - self.add_output("meta", socket_type="dict") - self.add_output("character", socket_type="character") - self.add_output("action", socket_type="str") - self.add_output("subtype", socket_type="str") - - async def run(self, state: GraphState): - message = self.normalized_input_value("message") - source = self.normalized_input_value("source") - action = self.normalized_input_value("action") - meta = self.normalized_input_value("meta") - subtype = self.normalized_input_value("subtype") - character: "Character" = self.normalized_input_value("character") - - extra = {} - - if meta and isinstance(meta, dict): - extra["meta"] = meta - - message = scene_message.DirectorMessage( - message, source=source, action=action, subtype=subtype, **extra - ) - - if character is not None: - message.set_meta(character=character.name) - - self.set_output_values( - { - "message": message, - "source": source, - "meta": meta, - "character": character, - "action": action, - "subtype": subtype, - } - ) - - -@register("scene/message/UnpackMeta") -class UnpackMessageMeta(Node): - """ - Unpacks a message meta dictionary - into arguments - - Inputs: - - - meta: The meta dictionary - - Outputs: - - - agent_name: The agent name - - function_name: The function name - - arguments: The arguments dictionary - """ - - def __init__(self, title="Unpack Message Meta", **kwargs): - super().__init__(title=title, **kwargs) - - def setup(self): - self.add_input("meta", socket_type="dict") - self.add_output("agent_name", socket_type="str") - self.add_output("function_name", socket_type="str") - self.add_output("arguments", socket_type="dict") - - async def run(self, state: GraphState): - meta = self.get_input_value("meta") - - self.set_output_values( - { - "agent_name": meta["agent"], - "function_name": meta["function"], - "arguments": meta.get("arguments", {}).copy(), - } - ) - - -@register("scene/message/ToggleMessageContextVisibility") -class ToggleMessageContextVisibility(Node): - """ - Hide or show a message. Hidden messages are not displayed to the AI. - - Inputs: - - - message: The message object - - Properties: - - - hidden: Whether the message is hidden - - Outputs: - - - message: The message object - """ - - class Fields: - hidden = PropertyField( - name="hidden", - description="Whether the message is hidden", - type="bool", - default=False, - ) - - def __init__(self, title="Toggle Message Context Visibility", **kwargs): - super().__init__(title=title, **kwargs) - - def setup(self): - self.add_input("message", socket_type="message_object") - - self.set_property("hidden", False) - - self.add_output("message", socket_type="message_object") - - async def run(self, state: GraphState): - message = self.require_input("message") - hidden = self.get_property("hidden") - - if hidden: - message.hide() - else: - message.unhide() - - self.set_output_values({"message": message}) - - @register("input/WaitForInput") class WaitForInput(Node): """ diff --git a/src/talemate/game/engine/nodes/scene_messages.py b/src/talemate/game/engine/nodes/scene_messages.py new file mode 100644 index 00000000..128630ed --- /dev/null +++ b/src/talemate/game/engine/nodes/scene_messages.py @@ -0,0 +1,340 @@ +from typing import TYPE_CHECKING + +from .core import ( + Node, + GraphState, + UNRESOLVED, + PropertyField, +) +from .registry import register +import talemate.scene_message as scene_message + +if TYPE_CHECKING: + from talemate.character import Character + + +@register("scene/message/CharacterMessage") +class CharacterMessage(Node): + """ + Creates a character message from a character and a message + + Inputs: + + - character: The character object + - message: The message to send + - source: The source of the message - player or ai, so whether the message is result of user input or AI generated + - from_choice: For player messages this indicates that the message was generated from a choice selection, for ai sourced messages this indicates the instruction that was followed + + Properties: + + - source: The source of the message + + Outputs: + + - message: The message object (this is a scene_message.CharacterMessage instance) + """ + + class Fields: + source = PropertyField( + name="source", + description="The source of the message", + type="str", + default="player", + choices=[ + "player", + "ai", + ], + ) + + def __init__(self, title="Character Message", **kwargs): + super().__init__(title=title, **kwargs) + + def setup(self): + self.add_input("character", socket_type="character") + self.add_input("message", socket_type="str") + self.add_input("source", socket_type="str", optional=True) + self.add_input("from_choice", socket_type="str", optional=True) + + self.set_property("source", "player") + + self.add_output("message", socket_type="message_object") + + async def run(self, state: GraphState): + character: "Character" = self.get_input_value("character") + message = self.get_input_value("message") + source = self.get_input_value("source") + from_choice = self.get_input_value("from_choice") + + extra = {} + + if isinstance(from_choice, str): + extra["from_choice"] = from_choice + + # Capture the character's current avatar at message creation time + # (not default avatar - messages only use current_avatar if set) + if character.current_avatar: + extra["asset_id"] = character.current_avatar + extra["asset_type"] = "avatar" + + # prefix name: if not already prefixed + if not message.startswith(f"{character.name}: "): + message = f"{character.name}: {message}" + + message = scene_message.CharacterMessage(message, source=source, **extra) + + self.set_output_values({"message": message}) + + +@register("scene/message/NarratorMessage") +class NarratorMessage(Node): + """ + Creates a narrator message + + Inputs: + + - message: The message to send + - source: The source of the message - player or ai, so whether the message is result of user input or AI generated + - meta: A dictionary of meta information to attach to the message. This will generally be arguments and function name that was called on the narrator agent to generate the message and will be used when regenerating the message. + + Properties: + + - source: The source of the message + + Outputs: + + - message: The message object (this is a scene_message.NarratorMessage instance) + """ + + class Fields: + source = PropertyField( + name="source", + description="The source of the message", + type="str", + default="ai", + choices=[ + "player", + "ai", + ], + ) + + def __init__(self, title="Narrator Message", **kwargs): + super().__init__(title=title, **kwargs) + + def setup(self): + self.add_input("message", socket_type="str") + self.add_input("source", socket_type="str", optional=True) + self.add_input("meta", socket_type="dict", optional=True) + + self.set_property("source", "ai") + + self.add_output("message", socket_type="message_object") + + async def run(self, state: GraphState): + message = self.get_input_value("message") + source = self.get_input_value("source") + meta = self.get_input_value("meta") + + extra = {} + + if meta and isinstance(meta, dict): + extra["meta"] = meta + + message = scene_message.NarratorMessage(message, source=source, **extra) + + self.set_output_values({"message": message}) + + +@register("scene/message/DirectorMessage") +class DirectorMessage(Node): + """ + Creates a director message + + Inputs: + + - message: The message to send + - source: The source of the message - player or ai, so whether the message is result of user input or AI generated + - meta: A dictionary of meta information to attach to the message. Can hold the character name that the message is related to. + - character: The character object that the message is related to + + Properties: + + - source: The source of the message + - action: Describes the director action + + Outputs: + + - message: The message object (this is a scene_message.DirectorMessage instance) + """ + + class Fields: + source = PropertyField( + name="source", + description="The source of the message", + type="str", + default="ai", + choices=[ + "player", + "ai", + ], + ) + + action = PropertyField( + name="action", + description="Describes the director action", + type="str", + default="actor_instruction", + choices=[ + "actor_instruction", + "user_direction", + ], + ) + + subtype = PropertyField( + name="subtype", + description="The subtype of the director message, used for further categorization of the message", + type="str", + default=UNRESOLVED, + choices=[ + "function_call", + "user_direction", + ], + ) + + def __init__(self, title="Director Message", **kwargs): + super().__init__(title=title, **kwargs) + + def setup(self): + self.add_input("message", socket_type="str") + self.add_input("source", socket_type="str", optional=True) + self.add_input("meta", socket_type="dict", optional=True) + self.add_input("character", socket_type="character", optional=True) + self.add_input("action", socket_type="str", optional=True) + self.add_input("subtype", socket_type="str", optional=True) + + self.set_property("source", "ai") + self.set_property("action", "actor_instruction") + self.set_property("subtype", UNRESOLVED) + + self.add_output("message", socket_type="message_object") + self.add_output("source", socket_type="str") + self.add_output("meta", socket_type="dict") + self.add_output("character", socket_type="character") + self.add_output("action", socket_type="str") + self.add_output("subtype", socket_type="str") + + async def run(self, state: GraphState): + message = self.normalized_input_value("message") + source = self.normalized_input_value("source") + action = self.normalized_input_value("action") + meta = self.normalized_input_value("meta") + subtype = self.normalized_input_value("subtype") + character: "Character" = self.normalized_input_value("character") + + extra = {} + + if meta and isinstance(meta, dict): + extra["meta"] = meta + + message = scene_message.DirectorMessage( + message, source=source, action=action, subtype=subtype, **extra + ) + + if character is not None: + message.set_meta(character=character.name) + + self.set_output_values( + { + "message": message, + "source": source, + "meta": meta, + "character": character, + "action": action, + "subtype": subtype, + } + ) + + +@register("scene/message/UnpackMeta") +class UnpackMessageMeta(Node): + """ + Unpacks a message meta dictionary + into arguments + + Inputs: + + - meta: The meta dictionary + + Outputs: + + - agent_name: The agent name + - function_name: The function name + - arguments: The arguments dictionary + """ + + def __init__(self, title="Unpack Message Meta", **kwargs): + super().__init__(title=title, **kwargs) + + def setup(self): + self.add_input("meta", socket_type="dict") + self.add_output("agent_name", socket_type="str") + self.add_output("function_name", socket_type="str") + self.add_output("arguments", socket_type="dict") + + async def run(self, state: GraphState): + meta = self.get_input_value("meta") + + self.set_output_values( + { + "agent_name": meta["agent"], + "function_name": meta["function"], + "arguments": meta.get("arguments", {}).copy(), + } + ) + + +@register("scene/message/ToggleMessageContextVisibility") +class ToggleMessageContextVisibility(Node): + """ + Hide or show a message. Hidden messages are not displayed to the AI. + + Inputs: + + - message: The message object + + Properties: + + - hidden: Whether the message is hidden + + Outputs: + + - message: The message object + """ + + class Fields: + hidden = PropertyField( + name="hidden", + description="Whether the message is hidden", + type="bool", + default=False, + ) + + def __init__(self, title="Toggle Message Context Visibility", **kwargs): + super().__init__(title=title, **kwargs) + + def setup(self): + self.add_input("message", socket_type="message_object") + + self.set_property("hidden", False) + + self.add_output("message", socket_type="message_object") + + async def run(self, state: GraphState): + message = self.require_input("message") + hidden = self.get_property("hidden") + + if hidden: + message.hide() + else: + message.unhide() + + self.set_output_values({"message": message}) diff --git a/tests/test_nodes_scene.py b/tests/test_nodes_scene.py index 92694d2d..1a54d68c 100644 --- a/tests/test_nodes_scene.py +++ b/tests/test_nodes_scene.py @@ -23,9 +23,7 @@ from talemate.game.engine.nodes.core import ( ) from talemate.game.engine.nodes.scene import ( ActivateCharacter, - CharacterMessage as CharacterMessageNode, DeactivateCharacter, - DirectorMessage as DirectorMessageNode, GetCharacter, GetCharacterAttribute, GetCharacterDescription, @@ -41,7 +39,6 @@ from talemate.game.engine.nodes.scene import ( IsPlayerCharacter, ListCharacters, MakeCharacter, - NarratorMessage as NarratorMessageNode, RemoveAllCharacters, RemoveCharacter, RestoreScene, @@ -53,14 +50,19 @@ from talemate.game.engine.nodes.scene import ( SetDescription, SetIntroduction, SetTitle, - ToggleMessageContextVisibility, TriggerGameLoopActorIter, UnpackCharacter, UnpackInteractionState, - UnpackMessageMeta, UpdateCharacterData, WaitForInput, ) +from talemate.game.engine.nodes.scene_messages import ( + CharacterMessage as CharacterMessageNode, + DirectorMessage as DirectorMessageNode, + NarratorMessage as NarratorMessageNode, + ToggleMessageContextVisibility, + UnpackMessageMeta, +) import talemate.events as events import talemate.scene_message as scene_message