From 55761b0736861507dd6096a33d8235e216135520 Mon Sep 17 00:00:00 2001 From: Archento Date: Tue, 9 Apr 2024 11:43:41 +0200 Subject: [PATCH 1/3] add: capability to define persistent functionality to an edge --- .../dialogues/hardcoded_chitchat.py | 22 +++++++++--- .../experimental/dialogues/__init__.py | 36 ++++++++++++------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/python/examples/17-stateful-communication/dialogues/hardcoded_chitchat.py b/python/examples/17-stateful-communication/dialogues/hardcoded_chitchat.py index 9c2ea7eb..67d1c847 100644 --- a/python/examples/17-stateful-communication/dialogues/hardcoded_chitchat.py +++ b/python/examples/17-stateful-communication/dialogues/hardcoded_chitchat.py @@ -6,6 +6,7 @@ the messages that are expected to be exchanged. """ +from typing import Type from warnings import warn from uagents import Model @@ -95,7 +96,7 @@ class RejectChitChatDialogue(Model): async def start_chitchat( ctx: Context, sender: str, - _msg: type[Model], + _msg: Type[Model], ): ctx.logger.info(f"Received init message from {sender}. Accepting Dialogue.") await ctx.send(sender, AcceptChitChatDialogue()) @@ -104,7 +105,7 @@ async def start_chitchat( async def accept_chitchat( ctx: Context, sender: str, - _msg: type[Model], + _msg: Type[Model], ): ctx.logger.info( f"Dialogue session with {sender} was accepted. " @@ -116,7 +117,7 @@ async def accept_chitchat( async def conclude_chitchat( ctx: Context, sender: str, - _msg: type[Model], + _msg: Type[Model], ): ctx.logger.info(f"Received conclude message from: {sender}; accessing history:") ctx.logger.info(ctx.dialogue) @@ -125,7 +126,7 @@ async def conclude_chitchat( async def default( _ctx: Context, _sender: str, - _msg: type[Model], + _msg: Type[Model], ): warn( "There is no handler for this message, please add your own logic by " @@ -135,9 +136,20 @@ async def default( ) +async def persisting_function( + ctx: Context, + _sender: str, + _msg: Type[Model], +): + ctx.logger.info("I was not overwritten, hehe.") + + init_session.set_default_behaviour(InitiateChitChatDialogue, start_chitchat) start_dialogue.set_default_behaviour(AcceptChitChatDialogue, accept_chitchat) -cont_dialogue.set_default_behaviour(ChitChatDialogueMessage, default) +# cont_dialogue.set_default_behaviour(ChitChatDialogueMessage, default, persist=False) +cont_dialogue.set_default_behaviour( + ChitChatDialogueMessage, persisting_function, persist=True +) end_session.set_default_behaviour(ConcludeChitChatDialogue, conclude_chitchat) diff --git a/python/src/uagents/experimental/dialogues/__init__.py b/python/src/uagents/experimental/dialogues/__init__.py index e84f7c87..5acb75d4 100644 --- a/python/src/uagents/experimental/dialogues/__init__.py +++ b/python/src/uagents/experimental/dialogues/__init__.py @@ -10,7 +10,7 @@ from uagents import Context, Model, Protocol from uagents.storage import KeyValueStore -DEFAULT_SESSION_TIMEOUT_IN_SECONDS = 100 +DEFAULT_SESSION_TIMEOUT_IN_SECONDS = 60 TARGET_UUID_VERSION = 4 JsonStr = str @@ -50,7 +50,7 @@ def __init__( self.starter = False self.ender = False self._model = None - self._func = None + self._func = Optional[tuple[MessageCallback, bool]] @property def model(self) -> Optional[Type[Model]]: @@ -67,10 +67,17 @@ def func(self) -> MessageCallback: """The message handler that is associated with the edge.""" return self._func - def set_default_behaviour(self, model: Type[Model], func: MessageCallback): - """Set the default behaviour for the edge.""" + def set_default_behaviour( + self, model: Type[Model], func: MessageCallback, persist: bool = False + ): + """ + Set the default behaviour for the edge that will be overwritten if + a decorator defines a new function to be called. + """ + if self._model: + raise ValueError("Functionality already set for edge!") self._model = model - self._func = func + self._func = func, persist class Dialogue(Protocol): @@ -326,7 +333,7 @@ def is_starter(self, digest: str) -> bool: def is_ender(self, digest: str) -> bool: """ - Return True if the digest is the last message of the dialogue. + Return True if the digest is one of the last messages of the dialogue. False otherwise. """ return digest in [self._digest_by_edge[edge] for edge in self._ender] @@ -346,7 +353,7 @@ def _auto_add_message_handler(self) -> None: """Automatically add message handlers for edges with models.""" for edge in self._edges: if edge.model and edge.func: - self._add_message_handler(edge.model, edge.func, None, False) + self._add_message_handler(edge.model, edge.func[0], None, False) def update_state(self, digest: str, session_id: UUID) -> None: """ @@ -504,17 +511,22 @@ def _on_state_transition(self, edge_name: str, model: Type[Model]): if edge_name not in self._digest_by_edge: raise ValueError("Edge does not exist in the dialogue!") + persisting_function = None + edge = self.get_edge(edge_name) + if edge.func[1]: + persisting_function = edge.func[0] + def decorator_on_state_transition(func: MessageCallback): @functools.wraps(func) - def handler(*args, **kwargs): - return func(*args, **kwargs) + async def handler(*args, **kwargs): + if persisting_function: + await persisting_function(*args, **kwargs) + return await func(*args, **kwargs) - edge = self.get_edge(edge_name) self._update_transition_model(edge, model) - self._add_message_handler(model, func, None, False) + self._add_message_handler(model, handler, None, False) return handler - # NOTE: recalculate manifest after each update and re-register /w agent return decorator_on_state_transition @property From c9c38a771bb328c6d9fde55016c0eebda72e9e8a Mon Sep 17 00:00:00 2001 From: Archento Date: Tue, 9 Apr 2024 14:49:42 +0200 Subject: [PATCH 2/3] Update __init__.py fix: _func type declaration --- python/src/uagents/experimental/dialogues/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/src/uagents/experimental/dialogues/__init__.py b/python/src/uagents/experimental/dialogues/__init__.py index 5acb75d4..e6f19d6d 100644 --- a/python/src/uagents/experimental/dialogues/__init__.py +++ b/python/src/uagents/experimental/dialogues/__init__.py @@ -50,7 +50,7 @@ def __init__( self.starter = False self.ender = False self._model = None - self._func = Optional[tuple[MessageCallback, bool]] + self._func: Optional[tuple[MessageCallback, bool]] = None @property def model(self) -> Optional[Type[Model]]: From 01267a635045b31196d65ef89074305d95f4812e Mon Sep 17 00:00:00 2001 From: Archento Date: Wed, 10 Apr 2024 09:48:34 +0200 Subject: [PATCH 3/3] refactor: clear distinction between message and edge handlers --- .../dialogues/hardcoded_chitchat.py | 14 ++-- .../experimental/dialogues/__init__.py | 75 +++++++++++++------ 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/python/examples/17-stateful-communication/dialogues/hardcoded_chitchat.py b/python/examples/17-stateful-communication/dialogues/hardcoded_chitchat.py index 67d1c847..975c0865 100644 --- a/python/examples/17-stateful-communication/dialogues/hardcoded_chitchat.py +++ b/python/examples/17-stateful-communication/dialogues/hardcoded_chitchat.py @@ -144,13 +144,13 @@ async def persisting_function( ctx.logger.info("I was not overwritten, hehe.") -init_session.set_default_behaviour(InitiateChitChatDialogue, start_chitchat) -start_dialogue.set_default_behaviour(AcceptChitChatDialogue, accept_chitchat) -# cont_dialogue.set_default_behaviour(ChitChatDialogueMessage, default, persist=False) -cont_dialogue.set_default_behaviour( - ChitChatDialogueMessage, persisting_function, persist=True -) -end_session.set_default_behaviour(ConcludeChitChatDialogue, conclude_chitchat) +init_session.set_message_handler(InitiateChitChatDialogue, start_chitchat) +start_dialogue.set_message_handler(AcceptChitChatDialogue, accept_chitchat) + +cont_dialogue.set_message_handler(ChitChatDialogueMessage, default) +cont_dialogue.set_edge_handler(ChitChatDialogueMessage, persisting_function) + +end_session.set_message_handler(ConcludeChitChatDialogue, conclude_chitchat) class ChitChatDialogue(Dialogue): diff --git a/python/src/uagents/experimental/dialogues/__init__.py b/python/src/uagents/experimental/dialogues/__init__.py index e6f19d6d..fdaaa1ef 100644 --- a/python/src/uagents/experimental/dialogues/__init__.py +++ b/python/src/uagents/experimental/dialogues/__init__.py @@ -47,10 +47,11 @@ def __init__( self.description = description self.parent = parent self.child = child - self.starter = False - self.ender = False - self._model = None - self._func: Optional[tuple[MessageCallback, bool]] = None + self.starter: bool = False + self.ender: bool = False + self._model: Type[Model] = None + self._func: Optional[MessageCallback] = None + self._efunc: Optional[MessageCallback] = None @property def model(self) -> Optional[Type[Model]]: @@ -63,21 +64,39 @@ def model(self, model: Type[Model]) -> None: self._model = model @property - def func(self) -> MessageCallback: + def func(self) -> Optional[MessageCallback]: """The message handler that is associated with the edge.""" return self._func - def set_default_behaviour( - self, model: Type[Model], func: MessageCallback, persist: bool = False - ): + @func.setter + def func(self, func: MessageCallback) -> None: + """Set the message handler that will be called when a message is received.""" + self._func = func + + @property + def efunc(self) -> MessageCallback: + """The edge handler that is associated with the edge.""" + return self._efunc + + def set_edge_handler(self, model: Type[Model], func: MessageCallback): + """ + Set the edge handler that will be called when a message is received + This handler can not be overwritten by a decorator. + """ + if self._model and self._model is not model: + raise ValueError("Functionality already set with a different model!") + self._model = model + self._efunc = func + + def set_message_handler(self, model: Type[Model], func: MessageCallback): """ - Set the default behaviour for the edge that will be overwritten if + Set the default message handler for the edge that will be overwritten if a decorator defines a new function to be called. """ - if self._model: - raise ValueError("Functionality already set for edge!") + if self._model and self._model is not model: + raise ValueError("Functionality already set with a different model!") self._model = model - self._func = func, persist + self._func = func class Dialogue(Protocol): @@ -349,11 +368,27 @@ def is_finished(self, session_id: UUID) -> bool: """ return self.is_ender(self.get_current_state(session_id)) + def _build_function_handler(self, edge: Edge) -> MessageCallback: + """Build the function handler for a message.""" + + @functools.wraps(edge.func) + async def handler(ctx: Context, sender: str, message: Any): + if edge.efunc: + await edge.efunc(ctx, sender, message) + return await edge.func(ctx, sender, message) + + return handler + def _auto_add_message_handler(self) -> None: """Automatically add message handlers for edges with models.""" for edge in self._edges: if edge.model and edge.func: - self._add_message_handler(edge.model, edge.func[0], None, False) + self._add_message_handler( + edge.model, + self._build_function_handler(edge), + None, # no replies + False, # only verified + ) def update_state(self, digest: str, session_id: UUID) -> None: """ @@ -511,18 +546,10 @@ def _on_state_transition(self, edge_name: str, model: Type[Model]): if edge_name not in self._digest_by_edge: raise ValueError("Edge does not exist in the dialogue!") - persisting_function = None - edge = self.get_edge(edge_name) - if edge.func[1]: - persisting_function = edge.func[0] - def decorator_on_state_transition(func: MessageCallback): - @functools.wraps(func) - async def handler(*args, **kwargs): - if persisting_function: - await persisting_function(*args, **kwargs) - return await func(*args, **kwargs) - + edge = self.get_edge(edge_name) + edge.func = func + handler = self._build_function_handler(edge) self._update_transition_model(edge, model) self._add_message_handler(model, handler, None, False) return handler