From a8d56a0eaa462575575cfec6a0488502499e8b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Wed, 20 Mar 2024 10:38:45 +0100 Subject: [PATCH 1/6] Refactor global variable implementation --- nemoguardrails/colang/v2_x/runtime/eval.py | 7 ++++++- nemoguardrails/colang/v2_x/runtime/flows.py | 3 --- .../colang/v2_x/runtime/statemachine.py | 18 +++++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/nemoguardrails/colang/v2_x/runtime/eval.py b/nemoguardrails/colang/v2_x/runtime/eval.py index 53f7d38c6..98d08447e 100644 --- a/nemoguardrails/colang/v2_x/runtime/eval.py +++ b/nemoguardrails/colang/v2_x/runtime/eval.py @@ -117,7 +117,12 @@ def eval_expression(expr: str, context: dict) -> Any: if f"var_{var_name}" in expr_locals: continue - val = context.get(var_name, None) + # Check if it is a global variable + global_var_name = f"_global_{var_name}" + if global_var_name in context: + val = context.get(global_var_name, None) + else: + val = context.get(var_name, None) # We transform dicts to AttributeDict so we can access their keys as attributes # e.g. write things like $speaker.name diff --git a/nemoguardrails/colang/v2_x/runtime/flows.py b/nemoguardrails/colang/v2_x/runtime/flows.py index f00b3590c..b9adcfbd6 100644 --- a/nemoguardrails/colang/v2_x/runtime/flows.py +++ b/nemoguardrails/colang/v2_x/runtime/flows.py @@ -530,9 +530,6 @@ class FlowState: # All the arguments of a flow (e.g. flow bot say $utterance -> arguments = ["$utterance"]) arguments: List[str] = field(default_factory=list) - # Variables in the flow that are defined as global - global_variables: Set[str] = field(default_factory=set) - # Parent flow id parent_uid: Optional[str] = None diff --git a/nemoguardrails/colang/v2_x/runtime/statemachine.py b/nemoguardrails/colang/v2_x/runtime/statemachine.py index dde384537..fd305580d 100644 --- a/nemoguardrails/colang/v2_x/runtime/statemachine.py +++ b/nemoguardrails/colang/v2_x/runtime/statemachine.py @@ -143,8 +143,6 @@ def create_flow_instance( }, ) - # Add self reference to context - flow_state.context["self"] = flow_state # Add all the flow parameters for idx, param in enumerate(flow_config.parameters): flow_state.arguments.append(param.name) @@ -1104,7 +1102,7 @@ def slide( expr_val = eval_expression( element.expression, _get_eval_context(state, flow_state) ) - if element.key in flow_state.global_variables: + if f"_global_{element.key}" in flow_state.context: state.context.update({element.key: expr_val}) else: flow_state.context.update({element.key: expr_val}) @@ -1157,7 +1155,10 @@ def slide( head.position += 1 elif isinstance(element, Global): - flow_state.global_variables.add(element.name.lstrip("$")) + var_name = element.name.lstrip('$') + flow_state.context[f"_global_{var_name}"] = None + if var_name not in state.context: + state.context[var_name] = None head.position += 1 elif isinstance(element, CatchPatternFailure): @@ -2213,11 +2214,10 @@ def create_umim_event(event: Event, event_args: Dict[str, Any]) -> Dict[str, Any def _get_eval_context(state: State, flow_state: FlowState) -> dict: context = flow_state.context.copy() # Link global variables - for var in flow_state.global_variables: - if var in state.context: - context.update({var: state.context[var]}) - else: - context.update({var: None}) + for var in flow_state.context.keys(): + if var.startswith("_global_"): + context.update({var: state.context[var[8:]]}) # Add state as _state context.update({"_state": state}) + context.update({"self": flow_state}) return context From 11fc4986b87615b51b26724892bf6be37e8e652c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Thu, 21 Mar 2024 12:07:24 +0100 Subject: [PATCH 2/6] Move Colang error definitions and other functions to new file to avoid cyclical includes --- nemoguardrails/actions/v2_x/generation.py | 7 +-- nemoguardrails/colang/v2_x/lang/expansion.py | 7 +-- nemoguardrails/colang/v2_x/runtime/errors.py | 21 ++++++++ nemoguardrails/colang/v2_x/runtime/eval.py | 3 +- nemoguardrails/colang/v2_x/runtime/flows.py | 24 ++-------- nemoguardrails/colang/v2_x/runtime/runtime.py | 48 +++++++++++++++++-- .../colang/v2_x/runtime/statemachine.py | 6 ++- nemoguardrails/colang/v2_x/runtime/utils.py | 45 ----------------- nemoguardrails/rails/llm/config.py | 2 +- tests/utils.py | 7 +-- 10 files changed, 83 insertions(+), 87 deletions(-) create mode 100644 nemoguardrails/colang/v2_x/runtime/errors.py diff --git a/nemoguardrails/actions/v2_x/generation.py b/nemoguardrails/actions/v2_x/generation.py index 5aba962ad..3958a0368 100644 --- a/nemoguardrails/actions/v2_x/generation.py +++ b/nemoguardrails/actions/v2_x/generation.py @@ -33,11 +33,8 @@ ) from nemoguardrails.colang.v2_x.lang.colang_ast import Flow from nemoguardrails.colang.v2_x.lang.utils import new_uuid -from nemoguardrails.colang.v2_x.runtime.flows import ( - ActionEvent, - InternalEvent, - LlmResponseError, -) +from nemoguardrails.colang.v2_x.runtime.errors import LlmResponseError +from nemoguardrails.colang.v2_x.runtime.flows import ActionEvent, InternalEvent from nemoguardrails.colang.v2_x.runtime.statemachine import ( Event, InternalEvents, diff --git a/nemoguardrails/colang/v2_x/lang/expansion.py b/nemoguardrails/colang/v2_x/lang/expansion.py index 82f392f0f..52e747557 100644 --- a/nemoguardrails/colang/v2_x/lang/expansion.py +++ b/nemoguardrails/colang/v2_x/lang/expansion.py @@ -39,11 +39,8 @@ When, While, ) -from nemoguardrails.colang.v2_x.runtime.flows import ( - ColangSyntaxError, - FlowConfig, - InternalEvents, -) +from nemoguardrails.colang.v2_x.runtime.errors import ColangSyntaxError +from nemoguardrails.colang.v2_x.runtime.flows import FlowConfig, InternalEvents from nemoguardrails.colang.v2_x.runtime.utils import new_var_uid diff --git a/nemoguardrails/colang/v2_x/runtime/errors.py b/nemoguardrails/colang/v2_x/runtime/errors.py new file mode 100644 index 000000000..7324322bd --- /dev/null +++ b/nemoguardrails/colang/v2_x/runtime/errors.py @@ -0,0 +1,21 @@ +"""Colang error types.""" + + +class ColangParsingError(Exception): + """Raised when there is invalid Colang syntax detected.""" + + +class ColangSyntaxError(Exception): + """Raised when there is invalid Colang syntax detected.""" + + +class ColangValueError(Exception): + """Raised when there is an invalid value detected in a Colang expression.""" + + +class ColangRuntimeError(Exception): + """Raised when there is a Colang related runtime exception.""" + + +class LlmResponseError(Exception): + """Raised when there is an issue with the lmm response.""" diff --git a/nemoguardrails/colang/v2_x/runtime/eval.py b/nemoguardrails/colang/v2_x/runtime/eval.py index 98d08447e..bac4a0895 100644 --- a/nemoguardrails/colang/v2_x/runtime/eval.py +++ b/nemoguardrails/colang/v2_x/runtime/eval.py @@ -23,7 +23,8 @@ from nemoguardrails.colang.v2_x.lang.colang_ast import Element from nemoguardrails.colang.v2_x.runtime import system_functions -from nemoguardrails.colang.v2_x.runtime.flows import ColangValueError, FlowState, State +from nemoguardrails.colang.v2_x.runtime.errors import ColangValueError +from nemoguardrails.colang.v2_x.runtime.flows import FlowState, State from nemoguardrails.colang.v2_x.runtime.utils import AttributeDict from nemoguardrails.eval.cli.simplify_formatter import SimplifyFormatter from nemoguardrails.utils import new_uid diff --git a/nemoguardrails/colang/v2_x/runtime/flows.py b/nemoguardrails/colang/v2_x/runtime/flows.py index b9adcfbd6..860a8d0da 100644 --- a/nemoguardrails/colang/v2_x/runtime/flows.py +++ b/nemoguardrails/colang/v2_x/runtime/flows.py @@ -23,7 +23,7 @@ from dataclasses import dataclass, field from datetime import datetime from enum import Enum -from typing import Any, Callable, Deque, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Callable, Deque, Dict, List, Optional, Tuple, Union from dataclasses_json import dataclass_json @@ -33,6 +33,8 @@ FlowParamDef, FlowReturnMemberDef, ) +from nemoguardrails.colang.v2_x.runtime.errors import ColangSyntaxError +from nemoguardrails.colang.v2_x.runtime.utils import new_readable_uid from nemoguardrails.utils import new_uid log = logging.getLogger(__name__) @@ -729,23 +731,3 @@ class State: event_matching_heads_reverse_map: Dict[Tuple[str, str], str] = field( default_factory=dict ) - - -class ColangParsingError(Exception): - """Raised when there is invalid Colang syntax detected.""" - - -class ColangSyntaxError(Exception): - """Raised when there is invalid Colang syntax detected.""" - - -class ColangValueError(Exception): - """Raised when there is an invalid value detected in a Colang expression.""" - - -class ColangRuntimeError(Exception): - """Raised when there is a Colang related runtime exception.""" - - -class LlmResponseError(Exception): - """Raised when there is an issue with the lmm response.""" diff --git a/nemoguardrails/colang/v2_x/runtime/runtime.py b/nemoguardrails/colang/v2_x/runtime/runtime.py index 915ee0941..446e5fba8 100644 --- a/nemoguardrails/colang/v2_x/runtime/runtime.py +++ b/nemoguardrails/colang/v2_x/runtime/runtime.py @@ -27,11 +27,11 @@ from nemoguardrails.colang import parse_colang_file from nemoguardrails.colang.runtime import Runtime from nemoguardrails.colang.v2_x.lang.colang_ast import Flow -from nemoguardrails.colang.v2_x.runtime.flows import ( +from nemoguardrails.colang.v2_x.runtime.errors import ( ColangRuntimeError, - Event, - FlowStatus, + ColangSyntaxError, ) +from nemoguardrails.colang.v2_x.runtime.flows import Event, FlowStatus from nemoguardrails.colang.v2_x.runtime.statemachine import ( FlowConfig, InternalEvent, @@ -41,7 +41,6 @@ initialize_state, run_to_completion, ) -from nemoguardrails.colang.v2_x.runtime.utils import create_flow_configs_from_flow_list from nemoguardrails.rails.llm.config import RailsConfig from nemoguardrails.utils import new_event_dict @@ -604,3 +603,44 @@ async def _run_action( "context_updates": context_updates, "start_action_event": start_action_event, } + + +def create_flow_configs_from_flow_list(flows: List[Flow]) -> Dict[str, FlowConfig]: + """Create a flow config dictionary and resolves flow overriding.""" + flow_configs: Dict[str, FlowConfig] = {} + override_flows: Dict[str, FlowConfig] = {} + + # Create two dictionaries with normal and override flows + for flow in flows: + assert isinstance(flow, Flow) + config = FlowConfig( + id=flow.name, + elements=flow.elements, + decorators={decorator.name: decorator for decorator in flow.decorators}, + parameters=flow.parameters, + return_members=flow.return_members, + source_code=flow.source_code, + ) + + if config.is_override: + if flow.name in override_flows: + raise ColangSyntaxError( + f"Multiple override flows with name '{flow.name}' detected! There can only be one!" + ) + override_flows[flow.name] = config + elif flow.name in flow_configs: + raise ColangSyntaxError( + f"Multiple non-overriding flows with name '{flow.name}' detected! There can only be one!" + ) + else: + flow_configs[flow.name] = config + + # Override normal flows + for override_flow in override_flows.values(): + if override_flow.id not in flow_configs: + raise ColangSyntaxError( + f"Override flow with name '{override_flow.id}' does not override any flow with that name!" + ) + flow_configs[override_flow.id] = override_flow + + return flow_configs diff --git a/nemoguardrails/colang/v2_x/runtime/statemachine.py b/nemoguardrails/colang/v2_x/runtime/statemachine.py index fd305580d..59225e92c 100644 --- a/nemoguardrails/colang/v2_x/runtime/statemachine.py +++ b/nemoguardrails/colang/v2_x/runtime/statemachine.py @@ -47,6 +47,10 @@ WaitForHeads, ) from nemoguardrails.colang.v2_x.lang.expansion import expand_elements +from nemoguardrails.colang.v2_x.runtime.errors import ( + ColangRuntimeError, + ColangValueError, +) from nemoguardrails.colang.v2_x.runtime.eval import ( ComparisonExpression, eval_expression, @@ -55,8 +59,6 @@ Action, ActionEvent, ActionStatus, - ColangRuntimeError, - ColangValueError, Event, FlowConfig, FlowHead, diff --git a/nemoguardrails/colang/v2_x/runtime/utils.py b/nemoguardrails/colang/v2_x/runtime/utils.py index ef3626083..e623d3733 100644 --- a/nemoguardrails/colang/v2_x/runtime/utils.py +++ b/nemoguardrails/colang/v2_x/runtime/utils.py @@ -14,10 +14,6 @@ # limitations under the License. import uuid -from typing import Dict, List - -from nemoguardrails.colang.v2_x.lang.colang_ast import Flow -from nemoguardrails.colang.v2_x.runtime.flows import ColangSyntaxError, FlowConfig class AttributeDict(dict): @@ -44,44 +40,3 @@ def new_readable_uid(name: str) -> str: def new_var_uid() -> str: """Creates a new uuid that is compatible with variable names.""" return str(uuid.uuid4()).replace("-", "_") - - -def create_flow_configs_from_flow_list(flows: List[Flow]) -> Dict[str, FlowConfig]: - """Create a flow config dictionary and resolves flow overriding.""" - flow_configs: Dict[str, FlowConfig] = {} - override_flows: Dict[str, FlowConfig] = {} - - # Create two dictionaries with normal and override flows - for flow in flows: - assert isinstance(flow, Flow) - config = FlowConfig( - id=flow.name, - elements=flow.elements, - decorators={decorator.name: decorator for decorator in flow.decorators}, - parameters=flow.parameters, - return_members=flow.return_members, - source_code=flow.source_code, - ) - - if config.is_override: - if flow.name in override_flows: - raise ColangSyntaxError( - f"Multiple override flows with name '{flow.name}' detected! There can only be one!" - ) - override_flows[flow.name] = config - elif flow.name in flow_configs: - raise ColangSyntaxError( - f"Multiple non-overriding flows with name '{flow.name}' detected! There can only be one!" - ) - else: - flow_configs[flow.name] = config - - # Override normal flows - for override_flow in override_flows.values(): - if override_flow.id not in flow_configs: - raise ColangSyntaxError( - f"Override flow with name '{override_flow.id}' does not override any flow with that name!" - ) - flow_configs[override_flow.id] = override_flow - - return flow_configs diff --git a/nemoguardrails/rails/llm/config.py b/nemoguardrails/rails/llm/config.py index dd563b4a8..b2a3f4463 100644 --- a/nemoguardrails/rails/llm/config.py +++ b/nemoguardrails/rails/llm/config.py @@ -24,7 +24,7 @@ from nemoguardrails.colang import parse_colang_file, parse_flow_elements from nemoguardrails.colang.v2_x.lang.colang_ast import Flow -from nemoguardrails.colang.v2_x.runtime.flows import ColangParsingError +from nemoguardrails.colang.v2_x.runtime.errors import ColangParsingError log = logging.getLogger(__name__) diff --git a/tests/utils.py b/tests/utils.py index 1f45d78cd..3fe3997b3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -27,10 +27,11 @@ from nemoguardrails import LLMRails, RailsConfig from nemoguardrails.colang import parse_colang_file -from nemoguardrails.colang.v2_x.lang.colang_ast import Decorator -from nemoguardrails.colang.v2_x.runtime.flows import FlowConfig, State +from nemoguardrails.colang.v2_x.runtime.flows import State +from nemoguardrails.colang.v2_x.runtime.runtime import ( + create_flow_configs_from_flow_list, +) from nemoguardrails.colang.v2_x.runtime.statemachine import initialize_state -from nemoguardrails.colang.v2_x.runtime.utils import create_flow_configs_from_flow_list from nemoguardrails.utils import EnhancedJsonEncoder, new_event_dict From a336416ed1116797d0fe990fb9ade4e1f8028d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Thu, 21 Mar 2024 12:24:55 +0100 Subject: [PATCH 3/6] Refactor internal flow event mechanism --- nemoguardrails/colang/v2_x/lang/expansion.py | 22 ++- nemoguardrails/colang/v2_x/runtime/flows.py | 99 +++++++--- nemoguardrails/colang/v2_x/runtime/runtime.py | 5 +- .../colang/v2_x/runtime/statemachine.py | 173 +++++------------- tests/v2_x/test_event_mechanics.py | 11 +- tests/v2_x/test_flow_mechanics.py | 42 ++++- tests/v2_x/test_story_mechanics.py | 8 +- tests/v2_x/test_various_mechanics.py | 5 +- 8 files changed, 190 insertions(+), 175 deletions(-) diff --git a/nemoguardrails/colang/v2_x/lang/expansion.py b/nemoguardrails/colang/v2_x/lang/expansion.py index 52e747557..40a2c75a2 100644 --- a/nemoguardrails/colang/v2_x/lang/expansion.py +++ b/nemoguardrails/colang/v2_x/lang/expansion.py @@ -166,11 +166,19 @@ def _expand_start_element( # Single element if element.spec.spec_type == SpecType.FLOW and element.spec.members is None: # It's a flow - # send StartFlow(flow_id="FLOW_NAME") + # $_instance_ = () + instance_uid_variable_name = f"_instance_uid_{new_var_uid()}" + new_elements.append( + Assignment( + key=instance_uid_variable_name, + expression=f"'({element.spec.name}){{uid()}}'", + ) + ) + # send StartFlow(flow_id=, flow_instance_uid=$_instance_) element.spec.arguments.update( { "flow_id": f"'{element.spec.name}'", - "flow_instance_uid": f"'{new_var_uid()}'", + "flow_instance_uid": f"'{{${instance_uid_variable_name}}}'", } ) new_elements.append( @@ -579,10 +587,18 @@ def _expand_activate_element( element_copy = copy.deepcopy(element) # TODO: Remove assert once SpecOp type is refactored assert isinstance(element_copy.spec, Spec) + # $_instance_ = () + instance_uid_variable_name = f"_instance_uid_{new_var_uid()}" + new_elements.append( + Assignment( + key=instance_uid_variable_name, + expression=f"'({element.spec.name}){{uid()}}'", + ) + ) element_copy.spec.arguments.update( { "flow_id": f"'{element.spec.name}'", - "flow_instance_uid": f"'{new_var_uid()}'", + "flow_instance_uid": f"'{{${instance_uid_variable_name}}}'", "activated": "True", } ) diff --git a/nemoguardrails/colang/v2_x/runtime/flows.py b/nemoguardrails/colang/v2_x/runtime/flows.py index 860a8d0da..f65dcacb2 100644 --- a/nemoguardrails/colang/v2_x/runtime/flows.py +++ b/nemoguardrails/colang/v2_x/runtime/flows.py @@ -530,11 +530,14 @@ class FlowState: priority: float = 1.0 # All the arguments of a flow (e.g. flow bot say $utterance -> arguments = ["$utterance"]) - arguments: List[str] = field(default_factory=list) + arguments: Dict[str, Any] = field(default_factory=dict) # Parent flow id parent_uid: Optional[str] = None + # Parent flow head id + parent_head_uid: Optional[str] = None + # The ids of all the child flows child_flow_uids: List[str] = field(default_factory=list) @@ -586,51 +589,78 @@ def __post_init__(self) -> None: "Failed": "failed_event", } - def get_event(self, name: str, arguments: dict) -> InternalEvent: + def get_event( + self, name: str, arguments: dict, matching_scores: Optional[List[float]] = None + ) -> InternalEvent: """Returns the corresponding action event.""" assert name in self._event_name_map, f"Event '{name}' not available!" func = getattr(self, self._event_name_map[name]) - return func(arguments) + if not matching_scores: + matching_scores = [] + return func(matching_scores, arguments) # Flow events to send - def start_event(self, _args: dict) -> InternalEvent: + def start_event( + self, matching_scores: List[float], args: Optional[dict] = None + ) -> InternalEvent: """Starts the flow. Takes no arguments.""" + arguments = { + "flow_instance_uid": new_readable_uid(self.flow_id), + "flow_id": self.flow_id, + "source_flow_instance_uid": self.parent_uid, + "source_head_uid": self.parent_head_uid, + "flow_hierarchy_position": self.hierarchy_position, + "activated": self.activated, + } + arguments.update(self.arguments) + if args: + arguments.update(args) return InternalEvent( - name=InternalEvents.START_FLOW, arguments={"flow_id": self.flow_id} + name=InternalEvents.START_FLOW, + arguments=arguments, + matching_scores=matching_scores, ) - def finish_event(self, _args: dict) -> InternalEvent: + def finish_event(self, matching_scores: List[float], _args: dict) -> InternalEvent: """Finishes the flow. Takes no arguments.""" return InternalEvent( name=InternalEvents.FINISH_FLOW, arguments={"flow_id": self.flow_id, "flow_instance_uid": self.uid}, + matching_scores=matching_scores, ) - def stop_event(self, _args: dict) -> InternalEvent: + def stop_event(self, matching_scores: List[float], _args: dict) -> InternalEvent: """Stops the flow. Takes no arguments.""" return InternalEvent( name="StopFlow", arguments={"flow_id": self.flow_id, "flow_instance_uid": self.uid}, + matching_scores=matching_scores, ) - def pause_event(self, _args: dict) -> InternalEvent: + def pause_event(self, matching_scores: List[float], _args: dict) -> InternalEvent: """Pauses the flow. Takes no arguments.""" return InternalEvent( name="PauseFlow", arguments={"flow_id": self.flow_id, "flow_instance_uid": self.uid}, + matching_scores=matching_scores, ) - def resume_event(self, _args: dict) -> InternalEvent: + def resume_event(self, matching_scores: List[float], _args: dict) -> InternalEvent: """Resumes the flow. Takes no arguments.""" return InternalEvent( name="ResumeFlow", arguments={"flow_id": self.flow_id, "flow_instance_uid": self.uid}, + matching_scores=matching_scores, ) # Flow events to match - def started_event(self, args: dict) -> InternalEvent: + def started_event( + self, matching_scores: List[float], args: Optional[Dict[str, Any]] = None + ) -> InternalEvent: """Returns the flow Started event.""" - return self._create_event(InternalEvents.FLOW_STARTED, args) + return self._create_out_event( + InternalEvents.FLOW_STARTED, matching_scores, args + ) # def paused_event(self, args: dict) -> FlowEvent: # """Returns the flow Pause event.""" @@ -640,29 +670,38 @@ def started_event(self, args: dict) -> InternalEvent: # """Returns the flow Resumed event.""" # return self._create_event(InternalEvents.FLOW_RESUMED, args) - def finished_event(self, args: dict) -> InternalEvent: + def finished_event( + self, matching_scores: List[float], args: Optional[Dict[str, Any]] = None + ) -> InternalEvent: """Returns the flow Finished event.""" - return self._create_event(InternalEvents.FLOW_FINISHED, args) + if not args: + args = {} + if "_return_value" in self.context: + args["return_value"] = self.context["_return_value"] + return self._create_out_event( + InternalEvents.FLOW_FINISHED, matching_scores, args + ) - def failed_event(self, args: dict) -> InternalEvent: + def failed_event( + self, matching_scores: List[float], args: Optional[Dict[str, Any]] = None + ) -> InternalEvent: """Returns the flow Failed event.""" - return self._create_event(InternalEvents.FLOW_FAILED, args) - - def _create_event(self, event_type: str, args: dict) -> InternalEvent: - arguments = args.copy() + return self._create_out_event(InternalEvents.FLOW_FAILED, matching_scores, args) + + def _create_out_event( + self, + event_type: str, + matching_scores: List[float], + args: Optional[Dict[str, Any]], + ) -> InternalEvent: + arguments = {} + arguments["source_flow_instance_uid"] = self.uid + arguments["flow_instance_uid"] = self.uid arguments["flow_id"] = self.flow_id - if "flow_instance_uid" in self.context: - arguments["flow_instance_uid"] = self.context["flow_instance_uid"] - arguments.update( - dict( - [ - (arg, self.context[arg]) - for arg in self.arguments - if arg in self.context - ] - ) - ) - return InternalEvent(event_type, arguments) + arguments.update(self.arguments) + if args: + arguments.update(args) + return InternalEvent(event_type, arguments, matching_scores) def __repr__(self) -> str: return ( diff --git a/nemoguardrails/colang/v2_x/runtime/runtime.py b/nemoguardrails/colang/v2_x/runtime/runtime.py index 446e5fba8..42b524c27 100644 --- a/nemoguardrails/colang/v2_x/runtime/runtime.py +++ b/nemoguardrails/colang/v2_x/runtime/runtime.py @@ -69,7 +69,10 @@ def __init__(self, config: RailsConfig, verbose: bool = False): async def _add_flows_action(self, state: "State", **args: dict) -> List[str]: log.info("Start AddFlowsAction! %s", args) flow_content = args["config"] - assert isinstance(flow_content, str) + if not isinstance(flow_content, str): + raise ColangRuntimeError( + "Parameter 'config' in AddFlowsAction is not of type 'str'!" + ) # Parse new flow try: parsed_flow = parse_colang_file( diff --git a/nemoguardrails/colang/v2_x/runtime/statemachine.py b/nemoguardrails/colang/v2_x/runtime/statemachine.py index 59225e92c..6a8c268e2 100644 --- a/nemoguardrails/colang/v2_x/runtime/statemachine.py +++ b/nemoguardrails/colang/v2_x/runtime/statemachine.py @@ -96,7 +96,7 @@ def initialize_state(state: State) -> None: # Create main flow state first main_flow_config = state.flow_configs["main"] main_flow = add_new_flow_instance( - state, create_flow_instance(main_flow_config, "0") + state, create_flow_instance(main_flow_config, new_readable_uid("main"), "0", {}) ) if main_flow_config.loop_id is None: main_flow.loop_id = new_readable_uid("main") @@ -117,7 +117,10 @@ def initialize_flow(state: State, flow_config: FlowConfig) -> None: def create_flow_instance( - flow_config: FlowConfig, flow_hierarchy_position: str + flow_config: FlowConfig, + flow_instance_uid: str, + flow_hierarchy_position: str, + event_arguments: Dict[str, Any], ) -> FlowState: """Create a new flow instance that can be added.""" loop_uid: Optional[str] = None @@ -128,18 +131,16 @@ def create_flow_instance( loop_uid = flow_config.loop_id # For type InteractionLoopType.PARENT we keep it None to infer loop_id at run_time from parent - flow_uid = new_readable_uid(flow_config.id) - head_uid = new_uid() flow_state = FlowState( - uid=flow_uid, + uid=flow_instance_uid, flow_id=flow_config.id, loop_id=loop_uid, hierarchy_position=flow_hierarchy_position, heads={ head_uid: FlowHead( uid=head_uid, - flow_state_uid=flow_uid, + flow_state_uid=flow_instance_uid, matching_scores=[], ) }, @@ -147,19 +148,28 @@ def create_flow_instance( # Add all the flow parameters for idx, param in enumerate(flow_config.parameters): - flow_state.arguments.append(param.name) + if param.name in event_arguments: + val = event_arguments[param.name] + else: + val = ( + eval_expression(param.default_value_expr, {}) + if param.default_value_expr + else None + ) + flow_state.arguments[param.name] = val flow_state.context.update( { - param.name: ( - eval_expression(param.default_value_expr, {}) - if param.default_value_expr - else None - ), + param.name: val, } ) + # Add the positional flow parameter identifiers for idx, param in enumerate(flow_config.parameters): - flow_state.arguments.append(f"${idx}") + positional_param = f"${idx}" + if positional_param in event_arguments: + val = event_arguments[positional_param] + flow_state.arguments[param.name] = val + flow_state.arguments[positional_param] = val # Add all flow return members for idx, member in enumerate(flow_config.return_members): @@ -459,7 +469,9 @@ def _process_internal_events_without_default_matchers( state, create_flow_instance( state.flow_configs[flow_id], + event.arguments["flow_instance_uid"], event.arguments["flow_hierarchy_position"], + event.arguments, ), ) elif event.name == InternalEvents.FINISH_FLOW: @@ -794,9 +806,7 @@ def _advance_head_front(state: State, heads: List[FlowHead]) -> List[FlowHead]: if flow_finished or all_heads_are_waiting: if flow_state.status == FlowStatus.STARTING: flow_state.status = FlowStatus.STARTED - event = create_internal_flow_event( - InternalEvents.FLOW_STARTED, flow_state, head.matching_scores - ) + event = flow_state.started_event(head.matching_scores) _push_internal_event(state, event) elif not flow_aborted: elem = get_element_from_head(state, head) @@ -844,10 +854,6 @@ def slide( """Try to slide a flow with the provided head.""" new_heads: List[FlowHead] = [] - # TODO: Implement global/local flow context handling - # context = state.context - # context = flow_state.context - while True: # if we reached the end, we stop if ( @@ -921,9 +927,7 @@ def slide( elif isinstance(element, Label): if element.name == "start_new_flow_instance": - new_event = _create_restart_flow_internal_event( - flow_state, head.matching_scores - ) + new_event = flow_state.start_event(head.matching_scores) _push_left_internal_event(state, new_event) flow_state.new_instance_started = True head.position += 1 @@ -1157,7 +1161,7 @@ def slide( head.position += 1 elif isinstance(element, Global): - var_name = element.name.lstrip('$') + var_name = element.name.lstrip("$") flow_state.context[f"_global_{var_name}"] = None if var_name not in state.context: state.context[var_name] = None @@ -1227,6 +1231,7 @@ def _start_flow(state: State, flow_state: FlowState, event_arguments: dict) -> N parent_flow = state.flow_states[parent_flow_uid] flow_state.parent_uid = parent_flow_uid parent_flow.child_flow_uids.append(flow_state.uid) + flow_state.parent_head_uid = event_arguments["source_head_uid"] loop_id = state.flow_configs[flow_state.flow_id].loop_id if loop_id is not None: @@ -1236,12 +1241,15 @@ def _start_flow(state: State, flow_state: FlowState, event_arguments: dict) -> N flow_state.loop_id = loop_id else: flow_state.loop_id = parent_flow.loop_id - flow_state.context.update({"loop_id": flow_state.loop_id}) flow_state.activated = event_arguments.get("activated", False) # Update context with event/flow parameters # TODO: Check if we really need all arguments int the context - flow_state.context.update(event_arguments) + # flow_state.context.update(event_arguments) + # Inherit parent context + # context = event_arguments.get("context", None) + # if context: + # flow_state.context = context # Resolve positional flow parameters to their actual name in the flow last_idx = -1 for idx, arg in enumerate(flow_state.arguments): @@ -1293,9 +1301,7 @@ def _abort_flow( flow_state.status = FlowStatus.STOPPED # Generate FlowFailed event - event = create_internal_flow_event( - InternalEvents.FLOW_FAILED, flow_state, matching_scores - ) + event = flow_state.failed_event(matching_scores) _push_internal_event(state, event) log.info( @@ -1308,7 +1314,7 @@ def _abort_flow( and not deactivate_flow and not flow_state.new_instance_started ): - event = _create_restart_flow_internal_event(flow_state, matching_scores) + event = flow_state.start_event(head.matching_scores) _push_left_internal_event(state, event) flow_state.new_instance_started = True @@ -1380,9 +1386,7 @@ def _finish_flow( flow_state.status = FlowStatus.FINISHED # Generate FlowFinished event - event = create_internal_flow_event( - InternalEvents.FLOW_FINISHED, flow_state, matching_scores - ) + event = flow_state.finished_event(matching_scores) _push_internal_event(state, event) # Check if it was an user/bot intent/action flow a generate internal events @@ -1413,7 +1417,7 @@ def _finish_flow( or len(flow_state.flow_id) < 18 else flow_state.flow_id[18:] ), - "parameter": flow_state.context.get("$0", None), + "parameter": flow_state.arguments.get("$0", None), }, matching_scores, ) @@ -1444,7 +1448,7 @@ def _finish_flow( event_type, { "flow_id": flow_state.flow_id, - "parameter": flow_state.context.get("$0", None), + "parameter": flow_state.arguments.get("$0", None), "intent_flow_id": intent, }, matching_scores, @@ -1462,7 +1466,7 @@ def _finish_flow( and not deactivate_flow and not flow_state.new_instance_started ): - event = _create_restart_flow_internal_event(flow_state, matching_scores) + event = flow_state.start_event(head.matching_scores) _push_left_internal_event(state, event) flow_state.new_instance_started = True @@ -1923,9 +1927,11 @@ def get_event_name_from_element( # Flow object assert element_spec.name flow_config = state.flow_configs[element_spec.name] - temp_flow_state = create_flow_instance(flow_config, "") + temp_flow_state = create_flow_instance(flow_config, "", "", {}) flow_event_name = element_spec.members[0]["name"] flow_event: InternalEvent = temp_flow_state.get_event(flow_event_name, {}) + del flow_event.arguments["source_flow_instance_uid"] + del flow_event.arguments["flow_instance_uid"] return flow_event.name elif element_spec.spec_type == SpecType.ACTION: # Action object @@ -2011,7 +2017,7 @@ def get_event_from_element( if element_spec.spec_type == SpecType.FLOW: # Flow object flow_config = state.flow_configs[element_spec.name] - temp_flow_state = create_flow_instance(flow_config, "") + temp_flow_state = create_flow_instance(flow_config, "", "", {}) flow_event_name = element_spec.members[0]["name"] flow_event_arguments = element_spec.members[0]["arguments"] flow_event_arguments = _evaluate_arguments( @@ -2020,6 +2026,8 @@ def get_event_from_element( flow_event: InternalEvent = temp_flow_state.get_event( flow_event_name, flow_event_arguments ) + del flow_event.arguments["source_flow_instance_uid"] + del flow_event.arguments["flow_instance_uid"] if element["op"] == "match": # Delete flow reference from event since it is only a helper object flow_event.flow = None @@ -2099,95 +2107,6 @@ def _generate_action_event_from_actionable_element( # state.next_steps_comment = element.get("_source_mapping", {}).get("comment") -def _create_restart_flow_internal_event( - flow_state: FlowState, matching_scores: List[float] -) -> InternalEvent: - # TODO: Check if this creates unwanted side effects of arguments being passed and keeping their state - arguments = dict( - [ - (arg, flow_state.context[arg]) - for arg in flow_state.arguments - if arg in flow_state.context - ] - ) - arguments.update( - { - "flow_id": flow_state.context["flow_id"], - "source_flow_instance_uid": flow_state.context["source_flow_instance_uid"], - "source_head_uid": flow_state.context["source_head_uid"], - "flow_hierarchy_position": flow_state.context["flow_hierarchy_position"], - "activated": flow_state.context["activated"], - } - ) - return create_internal_event(InternalEvents.START_FLOW, arguments, matching_scores) - - -def create_finish_flow_internal_event( - flow_instance_uid: str, - source_flow_instance_uid: str, - matching_scores: List[float], -) -> InternalEvent: - """Returns 'FinishFlow' internal event""" - arguments = { - "flow_instance_uid": flow_instance_uid, - "source_flow_instance_uid": source_flow_instance_uid, - } - return create_internal_event( - InternalEvents.FINISH_FLOW, - arguments, - matching_scores, - ) - - -def create_stop_flow_internal_event( - flow_instance_uid: str, - source_flow_instance_uid: str, - matching_scores: List[float], - deactivate_flow: bool = False, -) -> InternalEvent: - """Returns 'StopFlow' internal event""" - arguments: Dict[str, Any] = { - "flow_instance_uid": flow_instance_uid, - "source_flow_instance_uid": source_flow_instance_uid, - } - if deactivate_flow: - arguments["activated"] = False - - return create_internal_event( - InternalEvents.STOP_FLOW, - arguments, - matching_scores, - ) - - -def create_internal_flow_event( - event_name: str, - source_flow_state: FlowState, - matching_scores: List[float], - arguments: Optional[dict] = None, -) -> InternalEvent: - """Creates and returns a internal flow event""" - if arguments is None: - arguments = dict() - for arg in source_flow_state.arguments: - if arg in source_flow_state.context: - arguments.update({arg: source_flow_state.context[arg]}) - arguments.update( - { - "source_flow_instance_uid": source_flow_state.uid, - "flow_id": source_flow_state.flow_id, - "return_value": source_flow_state.context.get("_return_value", None), - } - ) - if "flow_instance_uid" in source_flow_state.context: - arguments["flow_instance_uid"] = source_flow_state.context["flow_instance_uid"] - return create_internal_event( - event_name, - arguments, - matching_scores, - ) - - def create_internal_event( event_name: str, event_args: dict, matching_scores: List[float] ) -> InternalEvent: diff --git a/tests/v2_x/test_event_mechanics.py b/tests/v2_x/test_event_mechanics.py index e0e0ca8eb..0b6df46c8 100644 --- a/tests/v2_x/test_event_mechanics.py +++ b/tests/v2_x/test_event_mechanics.py @@ -1282,6 +1282,12 @@ def test_match_hierarchy_of_internal_events(): flow _bot_say $text await UtteranceBotAction(script=$text) as $action + flow bot say $text + await _bot_say $text + + flow bot started saying something + match FlowStarted(flow_id="_bot_say") as $event + flow bot express $text await _bot_say $text @@ -1289,12 +1295,9 @@ def test_match_hierarchy_of_internal_events(): await bot express "hi" or bot express "hello" - flow bot started saying something - match FlowStarted(flow_id="_bot_say") as $event - flow a await bot started saying something - _bot_say "test" + bot say "test" flow main activate a diff --git a/tests/v2_x/test_flow_mechanics.py b/tests/v2_x/test_flow_mechanics.py index c43ee5c1e..e4fa250dd 100644 --- a/tests/v2_x/test_flow_mechanics.py +++ b/tests/v2_x/test_flow_mechanics.py @@ -521,13 +521,13 @@ def test_activate_flow_mechanism(): """Test the activate a flow mechanism.""" content = """ - flow a - start UtteranceBotAction(script="Start") + flow a $text + start UtteranceBotAction(script=$text) match UtteranceUserAction().Finished(final_transcript="Hi") start UtteranceBotAction(script="End") flow main - activate a + activate a "Start" match WaitAction().Finished() """ @@ -1413,5 +1413,39 @@ def test_flow_overriding(): ) +def test_flow_parameter_await_mechanism(): + """Test flow overriding mechanic.""" + + content = """ + flow a $text $test + $text = "bye" + $test = 32 + start UtteranceBotAction(script="{$text} {$test}") + + flow main + await a "hi" $test=123 + await UtteranceBotAction(script="Success") + match Event() + """ + + state = run_to_completion(_init_state(content), start_main_flow_event) + assert is_data_in_events( + state.outgoing_events, + [ + { + "type": "StartUtteranceBotAction", + "script": "bye 32", + }, + { + "type": "StopUtteranceBotAction", + }, + { + "type": "StartUtteranceBotAction", + "script": "Success", + }, + ], + ) + + if __name__ == "__main__": - test_flow_overriding() + test_flow_parameter_await_mechanism() diff --git a/tests/v2_x/test_story_mechanics.py b/tests/v2_x/test_story_mechanics.py index 5dfd19683..f2e2522d6 100644 --- a/tests/v2_x/test_story_mechanics.py +++ b/tests/v2_x/test_story_mechanics.py @@ -187,14 +187,14 @@ def test_when_conflict_issue(): assert is_data_in_events( state.outgoing_events, [ - { - "type": "StartGestureBotAction", - "gesture": "test", - }, { "type": "StartUtteranceBotAction", "script": "Ok", }, + { + "type": "StartGestureBotAction", + "gesture": "test", + }, ], ) diff --git a/tests/v2_x/test_various_mechanics.py b/tests/v2_x/test_various_mechanics.py index 9aefd9a7a..d24ede31a 100644 --- a/tests/v2_x/test_various_mechanics.py +++ b/tests/v2_x/test_various_mechanics.py @@ -125,8 +125,9 @@ def test_multi_level_member_match_from_reference(): match UtteranceUserAction.Finished(final_transcript="Done") flow main - send StartFlow(flow_id="a") - match FlowStarted(flow_id="a") as $event_ref + $flow_instance_uid = "(a){uid()}" + send StartFlow(flow_id="a", flow_instance_uid=$flow_instance_uid) + match FlowStarted(flow_id="a", flow_instance_uid=$flow_instance_uid) as $event_ref match $event_ref.flow.Finished() start UtteranceBotAction(script="End") """ From d04737bd5e1c7a355526871bdf7a8d9437251c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Thu, 21 Mar 2024 12:25:16 +0100 Subject: [PATCH 4/6] Update core libraries --- nemoguardrails/colang/v2_x/library/avatars.co | 17 ++++++++++------- nemoguardrails/colang/v2_x/library/core.co | 10 +++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/nemoguardrails/colang/v2_x/library/avatars.co b/nemoguardrails/colang/v2_x/library/avatars.co index 364273029..63e2ef619 100644 --- a/nemoguardrails/colang/v2_x/library/avatars.co +++ b/nemoguardrails/colang/v2_x/library/avatars.co @@ -30,6 +30,9 @@ # - Internal events: new_flow_start_uid -> new_flow_instance_uid # - Cleanup and improvements of certain log and print statements #------- +# 0.2.1 (3/20/2024) +# - Introduce $self variable to access flow attributes like loop_id +#------- ################################################################ # ----------------------------------- @@ -59,7 +62,7 @@ flow user said something -> $transcript $transcript = $event.final_transcript flow user said something unexpected -> $transcript - match UnhandledEvent(event="UtteranceUserActionFinished", loop_ids={$loop_id}) as $event + match UnhandledEvent(event="UtteranceUserActionFinished", loop_ids={$self.loop_id}) as $event send UserActionLog(flow_id="user said", parameter=$event.final_transcript, intent_flow_id=None) $transcript = $event.final_transcript @@ -79,7 +82,7 @@ flow user has selected choice $choice_id match VisualChoiceSceneAction.ChoiceUpdated(current_choice=[$choice_id]) as $event flow unhandled user intent -> $intent - match UnhandledEvent(event="FinishFlow", flow_id=regex("^user "), loop_ids={$loop_id}) as $event + match UnhandledEvent(event="FinishFlow", flow_id=regex("^user "), loop_ids={$self.loop_id}) as $event $intent = $event.flow_id @loop("user_was_silent") @@ -399,7 +402,7 @@ flow repeating timer $timer_id $interval_s # await TimerBotAction(timer_name=$timer_id, duration=$interval_s) flow await_flow_by_name $flow_name - $new_flow_instance_uid = "{uid()}" + $new_flow_instance_uid = "($flow_name){uid()}" send StartFlow(flow_id=$flow_name, flow_instance_uid=$new_flow_instance_uid) match FlowStarted(flow_id=$flow_name, flow_instance_uid=$new_flow_instance_uid) as $event_ref match $event_ref.flow.Finished() @@ -436,7 +439,7 @@ flow polling llm request response $interval flow generating user intent for unhandled user utterance """This is the fallback flow that takes care of unhandled user utterances and will generate a user intent.""" global $bot_talking_state - match UnhandledEvent(event="UtteranceUserActionFinished", loop_ids={$loop_id}) as $event + match UnhandledEvent(event="UtteranceUserActionFinished", loop_ids={$self.loop_id}) as $event if $bot_talking_state == False $transcript = $event.final_transcript log "generating user intent for unhandled user utterance: {$transcript}" @@ -497,7 +500,7 @@ flow handling start of undefined flow if "{search('^user ',$event.flow_id)}" == "True" # We have an undefined user intent, so we just fake it to be started by this fallback flow - send FlowStarted(flow_id=$event.flow_id, flow_instance_uid=$event.arguments.flow_instance_uid) + send FlowStarted(flow_id=$event.flow_id, flow_instance_uid=$event.flow_instance_uid) # Once this fallback flow receives the user intent it will finish and therefore also trigger the original matcher match FlowFinished(flow_id=$event.flow_id) @@ -508,7 +511,7 @@ flow handling start of undefined flow $flow_source = await GenerateFlowFromNameAction(name=$event.flow_id) await AddFlowsAction(config=$flow_source) - $new_flow_instance_uid = "{uid()}" + $new_flow_instance_uid = "($event.flow_id){uid()}" send StartFlow(flow_id=$event.flow_id, flow_instance_uid=$new_flow_instance_uid) match FlowStarted(flow_id=$event.flow_id, flow_instance_uid=$new_flow_instance_uid) as $event_ref match $event_ref.flow.Finished() @@ -520,7 +523,7 @@ flow execute llm instruction $instructions await AddFlowsAction(config=$flow_info.body) - $new_flow_instance_uid = "{uid()}" + $new_flow_instance_uid = "($flow_info.name){uid()}" send StartFlow(flow_id=$flow_info.name, flow_instance_uid=$new_flow_instance_uid) match FlowStarted(flow_id=$flow_info.name, flow_instance_uid=$new_flow_instance_uid) as $event_ref match $event_ref.flow.Finished() diff --git a/nemoguardrails/colang/v2_x/library/core.co b/nemoguardrails/colang/v2_x/library/core.co index 3f8c7da68..084464554 100644 --- a/nemoguardrails/colang/v2_x/library/core.co +++ b/nemoguardrails/colang/v2_x/library/core.co @@ -1,16 +1,16 @@ flow user said $text -> $transcript # meta: user action match UtteranceUserAction.Finished(final_transcript=$text) as $event - $transcript = $event.arguments.final_transcript + $transcript = $event.final_transcript flow user said something -> $transcript match UtteranceUserAction.Finished() as $event - send UserActionLog(flow_id="user said", parameter=$event.arguments.final_transcript, intent_flow_id="user said something") - $transcript = $event.arguments.final_transcript + send UserActionLog(flow_id="user said", parameter=$event.final_transcript, intent_flow_id="user said something") + $transcript = $event.final_transcript flow unhandled user intent -> $intent - match UnhandledEvent(event="FinishFlow", flow_id=regex("^user "), loop_ids={$loop_id}) as $event - $intent = $event.arguments.flow_id + match UnhandledEvent(event="FinishFlow", flow_id=regex("^user "), loop_ids={$self.loop_id}) as $event + $intent = $event.flow_id flow _bot_say $text """It's an internal helper for higher semantic level flows""" From 0f84a903aed08b71dfc59d46d1bed3d734c8ada7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Thu, 21 Mar 2024 12:25:49 +0100 Subject: [PATCH 5/6] Extend VSCode launch settings --- .vscode/launch.json | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index f5c42161f..af4679b82 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,7 @@ } }, { - "name": "Debug story (current directory)", + "name": "Debug story very verbose (current directory)", "type": "debugpy", "request": "launch", "console": "integratedTerminal", @@ -30,7 +30,25 @@ } }, { - "name": "Run story verbose (current directory)", + "name": "Debug story verbose (current directory)", + "type": "debugpy", + "request": "launch", + "console": "integratedTerminal", + "module": "nemoguardrails", + "args": [ + "chat", + "--config=${fileDirname}", + "--verbose", + "--verbose-llm-calls", + "--debug-level=INFO" + ], + "justMyCode": true, + "env": { + "PYTHONPATH": "${workspaceFolder}${pathSeparator}${env:PYTHONPATH}" + } + }, + { + "name": "Debug story (current directory)", "type": "debugpy", "request": "launch", "console": "integratedTerminal", @@ -47,6 +65,18 @@ "PYTHONPATH": "${workspaceFolder}${pathSeparator}${env:PYTHONPATH}" } }, + { + "name": "Run story verbose (current directory)", + "type": "debugpy", + "request": "launch", + "console": "integratedTerminal", + "module": "nemoguardrails", + "args": ["chat", "--config=${fileDirname}", "--verbose-llm-calls"], + "justMyCode": true, + "env": { + "PYTHONPATH": "${workspaceFolder}${pathSeparator}${env:PYTHONPATH}" + } + }, { "name": "Run story (current directory)", "type": "debugpy", From 635f676c30417589c42e35e46c996382bc1b25c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Thu, 21 Mar 2024 15:49:47 +0100 Subject: [PATCH 6/6] Implement flow context sharing --- .../colang/v2_x/runtime/statemachine.py | 8 +++ tests/v2_x/test_flow_mechanics.py | 55 ++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/nemoguardrails/colang/v2_x/runtime/statemachine.py b/nemoguardrails/colang/v2_x/runtime/statemachine.py index 6a8c268e2..15e88eb76 100644 --- a/nemoguardrails/colang/v2_x/runtime/statemachine.py +++ b/nemoguardrails/colang/v2_x/runtime/statemachine.py @@ -146,6 +146,14 @@ def create_flow_instance( }, ) + if "context" in event_arguments: + if flow_config.parameters: + raise ColangRuntimeError( + f"Context cannot be shared to flows with parameters: '{flow_config.id}'" + ) + # Replace local context with context from parent flow (shared flow context) + flow_state.context = event_arguments["context"] + # Add all the flow parameters for idx, param in enumerate(flow_config.parameters): if param.name in event_arguments: diff --git a/tests/v2_x/test_flow_mechanics.py b/tests/v2_x/test_flow_mechanics.py index e4fa250dd..4bd13f8d4 100644 --- a/tests/v2_x/test_flow_mechanics.py +++ b/tests/v2_x/test_flow_mechanics.py @@ -1447,5 +1447,58 @@ def test_flow_parameter_await_mechanism(): ) +def test_flow_context_sharing(): + """Test how a parent flow can share its context with a child flow.""" + + content = """ + flow a + start UtteranceBotAction(script="{$test1}") + $test0 = "pong" + $test1 = "bye" + $test2 = 55 + + flow b + global $test0 + start UtteranceBotAction(script="{$test0}") + + flow main + global $test0 + $test0 = "ping" + $test1 = "hi" + $instance_uid = uid() + send StartFlow(flow_id="a", flow_instance_uid=$instance_uid, context=$self.context) + match FlowStarted(flow_instance_uid=$instance_uid) + match FlowFinished(flow_instance_uid=$instance_uid) + start UtteranceBotAction(script="{$test1} {$test2}") + start b + match Event() + """ + + state = run_to_completion(_init_state(content), start_main_flow_event) + assert is_data_in_events( + state.outgoing_events, + [ + { + "type": "StartUtteranceBotAction", + "script": "hi", + }, + { + "type": "StopUtteranceBotAction", + }, + { + "type": "StartUtteranceBotAction", + "script": "bye 55", + }, + { + "type": "StartUtteranceBotAction", + "script": "pong", + }, + { + "type": "StopUtteranceBotAction", + }, + ], + ) + + if __name__ == "__main__": - test_flow_parameter_await_mechanism() + test_flow_context_sharing()