From 53a9809e2ed4a9075f918b18fbceaec864ac5570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Thu, 29 Aug 2024 11:44:32 +0200 Subject: [PATCH 01/14] Add interaction loop priority sorting --- nemoguardrails/colang/v2_x/runtime/flows.py | 13 +++- .../colang/v2_x/runtime/statemachine.py | 7 +- tests/v2_x/test_flow_mechanics.py | 66 ++++++++++++++++++- 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/nemoguardrails/colang/v2_x/runtime/flows.py b/nemoguardrails/colang/v2_x/runtime/flows.py index 47e71c85..7cba0147 100644 --- a/nemoguardrails/colang/v2_x/runtime/flows.py +++ b/nemoguardrails/colang/v2_x/runtime/flows.py @@ -393,6 +393,17 @@ def loop_id(self) -> Optional[str]: ) return None + @property + def loop_priority(self) -> int: + """Return the interaction loop priority (default: 0).""" + if "loop" in self.decorators: + parameters = self.decorators["loop"] + if "priority" in parameters: + return parameters["priority"] + elif "$1" in parameters: + return parameters["$1"] + return 0 + @property def loop_type(self) -> InteractionLoopType: """Return the interaction loop type.""" @@ -800,7 +811,7 @@ class State: # Helper dictionary that maps from flow_id (name) to all available flow states flow_id_states: Dict[str, List[FlowState]] = field(default_factory=dict) - # Helper dictionary () that maps active event matchers (by event names) to relevant heads (flow_state_uid, head_uid) + # Helper dictionary that maps active event matchers (by event names) to relevant heads (flow_state_uid, head_uid) event_matching_heads: Dict[str, List[Tuple[str, str]]] = field(default_factory=dict) # Helper dictionary that maps active heads (flow_state_uid, head_uid) to event matching names diff --git a/nemoguardrails/colang/v2_x/runtime/statemachine.py b/nemoguardrails/colang/v2_x/runtime/statemachine.py index 98dc8d91..2c8d562e 100644 --- a/nemoguardrails/colang/v2_x/runtime/statemachine.py +++ b/nemoguardrails/colang/v2_x/runtime/statemachine.py @@ -691,10 +691,13 @@ def _get_all_head_candidates(state: State, event: Event) -> List[Tuple[str, str] state.event_matching_heads.get(InternalEvents.FLOW_FINISHED, []) ) - # Ensure that event order is related to flow hierarchy + # Ensure that event order is related to interaction loop priority and secondly the flow hierarchy sorted_head_candidates = sorted( head_candidates, - key=lambda s: state.flow_states[s[0]].hierarchy_position, + key=lambda s: ( + -1 * state.flow_configs[state.flow_states[s[0]].flow_id].loop_priority, + state.flow_states[s[0]].hierarchy_position, + ), ) return sorted_head_candidates diff --git a/tests/v2_x/test_flow_mechanics.py b/tests/v2_x/test_flow_mechanics.py index 2f6a1861..7144b5ee 100644 --- a/tests/v2_x/test_flow_mechanics.py +++ b/tests/v2_x/test_flow_mechanics.py @@ -1490,6 +1490,70 @@ def test_interaction_loop_with_new(): ) +def test_interaction_loop_priorities(): + """Test that processing order of interaction loops dependent on their priority.""" + + content = """ + @loop("b", priority=5) + flow b + match Event1() + send EventB() + + @loop("c", 1) + flow c + match Event1() + send EventC() + + @loop("a", 10) + flow a + match Event2() + match Event1() + send EventA() + + flow main + activate a and c and b + """ + + state = run_to_completion(_init_state(content), start_main_flow_event) + assert is_data_in_events( + state.outgoing_events, + [], + ) + state = run_to_completion( + state, + { + "type": "Event1", + }, + ) + assert is_data_in_events( + state.outgoing_events, + [ + {"type": "EventB"}, + {"type": "EventC"}, + ], + ) + state = run_to_completion( + state, + { + "type": "Event2", + }, + ) + state = run_to_completion( + state, + { + "type": "Event1", + }, + ) + assert is_data_in_events( + state.outgoing_events, + [ + {"type": "EventA"}, + {"type": "EventB"}, + {"type": "EventC"}, + ], + ) + + def test_flow_overriding(): """Test flow overriding mechanic.""" @@ -2277,4 +2341,4 @@ def test_single_flow_activation_3(): if __name__ == "__main__": - test_deactivate_flow_mechanism() + test_interaction_loop_priorities() From fe756465983843a2c74024e67a27904ff2b5e043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Thu, 29 Aug 2024 11:45:24 +0200 Subject: [PATCH 02/14] Add check and error for positional after named parameters --- nemoguardrails/colang/v2_x/lang/transformer.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nemoguardrails/colang/v2_x/lang/transformer.py b/nemoguardrails/colang/v2_x/lang/transformer.py index 74675681..ed6ed922 100644 --- a/nemoguardrails/colang/v2_x/lang/transformer.py +++ b/nemoguardrails/colang/v2_x/lang/transformer.py @@ -44,6 +44,7 @@ When, While, ) +from nemoguardrails.colang.v2_x.runtime.errors import ColangSyntaxError class ColangTransformer(Transformer): @@ -233,8 +234,13 @@ def _spec_op(self, children: list, meta: Meta) -> SpecOp: def __parse_classical_arguments(self, arg_elements: list) -> dict: arguments = {} positional_index = 0 + named_parameter = None for arg_element in arg_elements: if arg_element["_type"] == "expr": + if named_parameter: + raise ColangSyntaxError( + f"Positional parameter '{arg_element['elements'][0]}' cannot be used after named parameter '{named_parameter}'! (Line: {arg_element['_source']['line']}, Column: {arg_element['_source']['column']})" + ) arguments[f"${positional_index}"] = arg_element["elements"][0] positional_index += 1 else: @@ -242,6 +248,10 @@ def __parse_classical_arguments(self, arg_elements: list) -> dict: if len(arg_element["elements"]) == 1: expr_element = arg_element["elements"][0] + if named_parameter: + raise ColangSyntaxError( + f"Positional parameter '{expr_element['elements'][0]}' cannot be used after named parameter '{named_parameter}'! (Line: {expr_element['_source']['line']}, Column: {expr_element['_source']['column']})" + ) assert expr_element["_type"] == "expr" arguments[f"${positional_index}"] = expr_element["elements"][0] positional_index += 1 @@ -250,6 +260,7 @@ def __parse_classical_arguments(self, arg_elements: list) -> dict: expr_el = arg_element["elements"][1] expr = expr_el["elements"][0] arguments[name] = expr + named_parameter = name return arguments def _spec(self, children: List[dict], _meta: Meta) -> Spec: From f677772d5ace832b048c4cb3881be1a1d07c04ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Thu, 29 Aug 2024 11:46:04 +0200 Subject: [PATCH 03/14] Add parameter variation --- tests/v2_x/test_flow_mechanics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/v2_x/test_flow_mechanics.py b/tests/v2_x/test_flow_mechanics.py index 7144b5ee..fcdeeb81 100644 --- a/tests/v2_x/test_flow_mechanics.py +++ b/tests/v2_x/test_flow_mechanics.py @@ -1504,7 +1504,7 @@ def test_interaction_loop_priorities(): match Event1() send EventC() - @loop("a", 10) + @loop(id="a", priority=10) flow a match Event2() match Event1() From 52a3861362d95159035ed3ff54463b25d8089729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Thu, 29 Aug 2024 11:48:11 +0200 Subject: [PATCH 04/14] Add loop priority to tracking flows --- nemoguardrails/colang/v2_x/library/avatars.co | 2 +- nemoguardrails/colang/v2_x/library/core.co | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nemoguardrails/colang/v2_x/library/avatars.co b/nemoguardrails/colang/v2_x/library/avatars.co index 7785e9f2..9d9e426b 100644 --- a/nemoguardrails/colang/v2_x/library/avatars.co +++ b/nemoguardrails/colang/v2_x/library/avatars.co @@ -146,7 +146,7 @@ flow bot started an action -> $action # State Tracking Flows # ----------------------------------- -@loop("state_tracking") +@loop("state_tracking", 10) flow tracking visual choice selection state global $choice_selection_state when VisualChoiceSceneAction.Started() diff --git a/nemoguardrails/colang/v2_x/library/core.co b/nemoguardrails/colang/v2_x/library/core.co index 4647e537..a09324be 100644 --- a/nemoguardrails/colang/v2_x/library/core.co +++ b/nemoguardrails/colang/v2_x/library/core.co @@ -159,7 +159,7 @@ flow bot refuse to respond # State Tracking Flows # ----------------------------------- -@loop("state_tracking") +@loop("state_tracking", 10) flow tracking bot talking state """tracking bot talking state in global variable $bot_talking_state.""" global $bot_talking_state @@ -177,7 +177,7 @@ flow tracking bot talking state $last_bot_script = $bot_said_flow.text $last_bot_message = $bot_said_flow.text -@loop("state_tracking") +@loop("state_tracking", 10) flow tracking user talking state """Track user utterance state in global variables: $user_talking_state, $last_user_transcript.""" global $user_talking_state From f17b334622e7b0a095e48aad8c7a284353770fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Thu, 29 Aug 2024 12:00:59 +0200 Subject: [PATCH 05/14] Update Colang doc with interaction loop priority --- docs/colang_2/language_reference/more-on-flows.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/colang_2/language_reference/more-on-flows.rst b/docs/colang_2/language_reference/more-on-flows.rst index 1691198c..93bbb257 100644 --- a/docs/colang_2/language_reference/more-on-flows.rst +++ b/docs/colang_2/language_reference/more-on-flows.rst @@ -297,12 +297,12 @@ So far, any concurrently progressing flows that resulted in different event gene .. code-block:: colang - @loop("") + @loop([id=]""[,[priority=]]) flow ... Hint: To generate a new loop name for each flow call use the loop name "NEW" -By default, any flow without an explicit interaction loop inherits the interaction loop of its parent flow. Let's see now an example of a second interaction loop to design flows that augment the main interaction rather than compete with it: +By default, any flow without an explicit interaction loop inherits the interaction loop of its parent flow and has priority level 0. Let's see now an example of a second interaction loop to design flows that augment the main interaction rather than compete with it: .. code-block:: colang :caption: more_on_flows/interaction_loops/main.co @@ -356,6 +356,8 @@ The example implements two bot reaction flows that listen to the user saying "Hi Goodbye +By default, parallel flows in different interaction loops advance in order of their start or activation. This might be an important detail if e.g a global variable is set in one flow and read in another. If the order is wrong, the global variable will not be set yet when read by the other flow. In order to enforce the processing order independent of the start or activation order, you can define the interaction loop priority level using an integer. By default, any interaction loop has priority 0. A higher number defines a higher priority, and lower (negative) number a lower processing priority. + .. _more-on-flows-flow-conflict-resolution-prioritization: From 72a729813777a1c04488780a765a9154d43dc449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Thu, 29 Aug 2024 12:03:00 +0200 Subject: [PATCH 06/14] Update Colang changelog --- CHANGELOG-Colang.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-Colang.md b/CHANGELOG-Colang.md index 4244ea4e..f4b5c76a 100644 --- a/CHANGELOG-Colang.md +++ b/CHANGELOG-Colang.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added * [#673](https://github.com/NVIDIA/NeMo-Guardrails/pull/673) Add support for new Colang 2 keyword `deactivate`. +* [#712](https://github.com/NVIDIA/NeMo-Guardrails/pull/712) Add interaction loop priority levels for flows. ### Changed From 685481239d9b67804358c4f55a494ba3cc1f073b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Fri, 30 Aug 2024 09:16:21 +0200 Subject: [PATCH 07/14] Start adding more functions --- nemoguardrails/cli/debugger.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nemoguardrails/cli/debugger.py b/nemoguardrails/cli/debugger.py index 49dbf3a0..3da28a8b 100644 --- a/nemoguardrails/cli/debugger.py +++ b/nemoguardrails/cli/debugger.py @@ -44,6 +44,11 @@ def set_output_state(_state: State): state = _state +@app.command() +def restart(): + """Restart the current Colang script.""" + + @app.command() def list_flows( active: bool = typer.Option(default=False, help="Only show active flows.") From 522352ea59cd2ce6ee495f29481d022b169dd346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Fri, 30 Aug 2024 11:01:32 +0200 Subject: [PATCH 08/14] Improve `!list-flows` debug command --- nemoguardrails/cli/debugger.py | 76 +++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/nemoguardrails/cli/debugger.py b/nemoguardrails/cli/debugger.py index 3da28a8b..7bca006a 100644 --- a/nemoguardrails/cli/debugger.py +++ b/nemoguardrails/cli/debugger.py @@ -21,8 +21,14 @@ from rich.tree import Tree from nemoguardrails.colang.v2_x.lang.colang_ast import SpecOp, SpecType -from nemoguardrails.colang.v2_x.runtime.flows import FlowState, State +from nemoguardrails.colang.v2_x.runtime.flows import ( + FlowConfig, + FlowState, + InteractionLoopType, + State, +) from nemoguardrails.colang.v2_x.runtime.runtime import RuntimeV2_x +from nemoguardrails.colang.v2_x.runtime.statemachine import _is_active_flow from nemoguardrails.utils import console runtime: Optional[RuntimeV2_x] = None @@ -44,33 +50,83 @@ def set_output_state(_state: State): state = _state -@app.command() -def restart(): - """Restart the current Colang script.""" +# @app.command() +# def restart(): +# """Restart the current Colang script.""" +# runtime.state = None +# runtime.input_events = [] +# runtime.first_time = True @app.command() def list_flows( - active: bool = typer.Option(default=False, help="Only show active flows.") + all: bool = typer.Option( + default=False, help="Show all flows (including inactive)." + ), + order_by_name: bool = typer.Option( + default=False, + help="Order flows by flow name, otherwise its ordered by event processing priority.", + ), ): + assert state + """List the flows from the current state.""" table = Table(header_style="bold magenta") - table.add_column("ID", style="dim", width=12) + table.add_column("ID", style="dim", width=9) table.add_column("Flow Name") + table.add_column("Loop") + table.add_column("Flow Instances") table.add_column("Source") + def get_loop_type(flow_config: FlowConfig) -> str: + if flow_config.loop_type == InteractionLoopType.NAMED: + return flow_config.loop_type.value + f" ('{flow_config.loop_id}')" + else: + return flow_config.loop_type.value + rows = [] for flow_id, flow_config in state.flow_configs.items(): source = flow_config.source_file - if "nemoguardrails" in source: + if source and "nemoguardrails" in source: source = source.rsplit("nemoguardrails", 1)[1] - # if active and state.flow_id_states[flow_id] - rows.append([flow_id, source]) + if not all: + # Show only active flows + active_instances = [] + if flow_id in state.flow_id_states: + for flow_instance in state.flow_id_states[flow_id]: + if _is_active_flow(flow_instance): + active_instances.append(flow_instance.uid.split(")")[1][:5]) + if active_instances: + rows.append( + [ + flow_id, + get_loop_type(state.flow_configs[flow_id]), + ",".join(active_instances), + source, + ] + ) + else: + instances = [] + if flow_id in state.flow_id_states: + instances = [ + i.uid.split(")")[1][:5] for i in state.flow_id_states[flow_id] + ] + rows.append( + [ + flow_id, + get_loop_type(state.flow_configs[flow_id]), + ",".join(instances), + source, + ] + ) - rows.sort(key=lambda x: x[0]) + if order_by_name: + rows.sort(key=lambda x: x[0]) + else: + rows.sort(key=lambda x: -state.flow_configs[x[0]].loop_priority) for i, row in enumerate(rows): table.add_row(f"{i+1}", *row) From b8276b12babff0cbe22e88a5be369f7f77ebba6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Fri, 30 Aug 2024 12:26:03 +0200 Subject: [PATCH 09/14] Update documentation --- .../development-and-debugging.rst | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/docs/colang_2/language_reference/development-and-debugging.rst b/docs/colang_2/language_reference/development-and-debugging.rst index 5c9e0d46..06c40cc0 100644 --- a/docs/colang_2/language_reference/development-and-debugging.rst +++ b/docs/colang_2/language_reference/development-and-debugging.rst @@ -13,8 +13,8 @@ Currently, the Colang story designing and building environment is fairly limited Integrated Development Environment (IDE) ----------------------------------------- -- We suggest using `Visual Studio Code `_ and the Colang highlighting extension (`Github link <../../../vscode_extension>`_) to help with highlighting Colang code. -- You can use the Visual Studio Code launch setting to run a Colang story with nemoguardrails in a python environment by pressing F5 (`launch.json <../../../.vscode/launch.json>`_) +- We suggest using `Visual Studio Code `_ and the Colang highlighting extension (`Github link `_) to help with highlighting Colang code. +- You can use the Visual Studio Code launch setting to run a Colang story with nemoguardrails in a python environment by pressing F5 (`launch.json `_) - You can show generated and received events by adding the ``--verbose`` flag when starting nemoguardrails, that will also show all generated LLM prompts and responses - To see even more details and show the internal run-time logs use ``--debug-level=INFO`` or set it equal to ``DEBUG`` @@ -37,7 +37,7 @@ To help debugging your Colang flows you can use the print statement ``print `` to append to the logging shown in the verbose mode, which will appear as "Colang debug info: ". +Alternatively, use the log statement ``log `` to append to the logging shown in the verbose mode, which will appear as "Colang Log :: ". Furthermore, the Colang function ``flows_info`` can be used to return more information about a flow instance: @@ -71,6 +71,49 @@ Furthermore, the Colang function ``flows_info`` can be used to return more infor Where ``pretty_str`` converts the returned dictionary object to a nicely formatted string. If no parameter is provided to the function it will return a dictionary containing all the currently active flow instances. +------------------------- +CLI Debugging Commands +------------------------- + +The NeMo Guardrail CLI provides a couple of additional debugging commands that always start with the ``!`` character, e.g.: + +.. code-block:: text + + > !list-flows + ┏━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ ID ┃ Flow Name ┃ Loop ┃ Flow Instances ┃ Source ┃ + ┡━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + │ 1 │ tracking visual choice selection state │ named ('state_tracking') │ 924ad │ /colang/v2_x/library/avatars.co │ + │ 2 │ tracking bot talking state │ named ('state_tracking') │ 28804 │ /colang/v2_x/library/core.co │ + │ 3 │ wait │ new │ 9e18b │ /colang/v2_x/library/timing.co │ + │ 4 │ user was silent │ named ('user_was_silent') │ c1dfd │ /colang/v2_x/library/timing.co │ + │ 5 │ polling llm request response │ named ('llm_response_polling') │ a003b │ /colang/v2_x/library/llm.co │ + │ 6 │ continuation on unhandled user utterance │ parent │ 342c6 │ /colang/v2_x/library/llm.co │ + │ 7 │ automating intent detection │ parent │ c5c0a │ /colang/v2_x/library/llm.co │ + │ 8 │ marking user intent flows │ named ('intent_log') │ ad2b4 │ /colang/v2_x/library/llm.co │ + │ 9 │ logging marked user intent flows │ named ('intent_log') │ a5dd9 │ /colang/v2_x/library/llm.co │ + │ 10 │ marking bot intent flows │ named ('intent_log') │ 8873e │ /colang/v2_x/library/llm.co │ + │ 11 │ logging marked bot intent flows │ named ('intent_log') │ 3d331 │ /colang/v2_x/library/llm.co │ + │ 12 │ user has selected choice │ parent │ 78b3b,783b6,c4186,cd667 │ /colang/v2_x/library/avatars.co │ + │ 13 │ user interrupted bot talking │ parent │ 6e576 │ /colang/v2_x/library/avatars.co │ + │ 14 │ bot posture │ parent │ d9f32 │ /colang/v2_x/library/avatars.co │ + │ 15 │ handling bot talking interruption │ named ('bot_interruption') │ 625f1 │ /colang/v2_x/library/avatars.co │ + │ 16 │ managing idle posture │ named ('managing_idle_posture') │ 0bfe3 │ /colang/v2_x/library/avatars.co │ + │ 17 │ _user_said │ parent │ db5e4,d2cb3,b7b85,095e0 │ /colang/v2_x/library/core.co │ + │ 18 │ _user_said_something_unexpected │ parent │ cb676 │ /colang/v2_x/library/core.co │ + │ 19 │ user said │ parent │ c4a05,45f2c,cd4ab,fecc2 │ /colang/v2_x/library/core.co │ + │ 20 │ bot started saying something │ parent │ fc2a7,8d5f1 │ /colang/v2_x/library/core.co │ + │ 21 │ notification of colang errors │ named ('colang_errors_warning') │ cd5a8 │ /colang/v2_x/library/core.co │ + │ 22 │ notification of undefined flow start │ parent │ 20d10 │ /colang/v2_x/library/core.co │ + │ 23 │ wait indefinitely │ parent │ 3713b │ /colang/v2_x/library/core.co │ + └────┴───────────────────────────────────────────┴─────────────────────────────────┴─────────────────────────┴─────────────────────────────────┘ + +.. code-block:: colang + :caption: All CLI debugging commands + + list-flows [--all] [--order_by_name] # Shows all active flows in a table in order of their interaction loop priority and name + tree # Shows the flow hierarchy tree of all active flows + ------------- Useful Flows ------------- From b7d24c703e8f519d19eefc0d90bcd09a1c1e197e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Fri, 30 Aug 2024 13:46:19 +0200 Subject: [PATCH 10/14] Improve loop infos --- nemoguardrails/cli/debugger.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/nemoguardrails/cli/debugger.py b/nemoguardrails/cli/debugger.py index 7bca006a..1237a6ca 100644 --- a/nemoguardrails/cli/debugger.py +++ b/nemoguardrails/cli/debugger.py @@ -76,15 +76,19 @@ def list_flows( table.add_column("ID", style="dim", width=9) table.add_column("Flow Name") - table.add_column("Loop") + table.add_column("Loop (Priority | Type | Id)") table.add_column("Flow Instances") table.add_column("Source") - def get_loop_type(flow_config: FlowConfig) -> str: + def get_loop_info(flow_config: FlowConfig) -> str: if flow_config.loop_type == InteractionLoopType.NAMED: - return flow_config.loop_type.value + f" ('{flow_config.loop_id}')" + return ( + f"{flow_config.loop_priority} │ " + + flow_config.loop_type.value.capitalize() + + f" │ '{flow_config.loop_id}'" + ) else: - return flow_config.loop_type.value + return f"{flow_config.loop_priority} │ " + flow_config.loop_type.value rows = [] for flow_id, flow_config in state.flow_configs.items(): @@ -103,7 +107,7 @@ def get_loop_type(flow_config: FlowConfig) -> str: rows.append( [ flow_id, - get_loop_type(state.flow_configs[flow_id]), + get_loop_info(state.flow_configs[flow_id]), ",".join(active_instances), source, ] @@ -117,7 +121,7 @@ def get_loop_type(flow_config: FlowConfig) -> str: rows.append( [ flow_id, - get_loop_type(state.flow_configs[flow_id]), + get_loop_info(state.flow_configs[flow_id]), ",".join(instances), source, ] @@ -126,7 +130,7 @@ def get_loop_type(flow_config: FlowConfig) -> str: if order_by_name: rows.sort(key=lambda x: x[0]) else: - rows.sort(key=lambda x: -state.flow_configs[x[0]].loop_priority) + rows.sort(key=lambda x: (-state.flow_configs[x[0]].loop_priority, x[0])) for i, row in enumerate(rows): table.add_row(f"{i+1}", *row) From c96101161a8441d07bb114f037993b03e96be191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Fri, 30 Aug 2024 17:09:07 +0200 Subject: [PATCH 11/14] Add more CLI debugging commands --- nemoguardrails/cli/chat.py | 170 +++++++++++++++++++-------------- nemoguardrails/cli/debugger.py | 54 +++++++++-- 2 files changed, 143 insertions(+), 81 deletions(-) diff --git a/nemoguardrails/cli/chat.py b/nemoguardrails/cli/chat.py index 0401f375..49f23802 100644 --- a/nemoguardrails/cli/chat.py +++ b/nemoguardrails/cli/chat.py @@ -14,6 +14,7 @@ # limitations under the License. import asyncio import os +from dataclasses import dataclass, field from typing import Dict, List, Optional, cast import aiohttp @@ -24,6 +25,7 @@ from nemoguardrails import LLMRails, RailsConfig from nemoguardrails.cli import debugger from nemoguardrails.colang.v2_x.runtime.eval import eval_expression +from nemoguardrails.colang.v2_x.runtime.flows import State from nemoguardrails.colang.v2_x.runtime.runtime import RuntimeV2_x from nemoguardrails.logging import verbose from nemoguardrails.logging.verbose import console @@ -122,34 +124,43 @@ async def _run_chat_v1_0( history.append(bot_message) -async def _run_chat_v2_x(rails_app: LLMRails): - """Simple chat loop for v2.x using the stateful events API.""" - state = None - waiting_user_input = False - running_timer_tasks: Dict[str, asyncio.Task] = {} - input_events: List[dict] = [] - output_events: List[dict] = [] - output_state = None - +@dataclass +class ChatState: + state: Optional[State] = None + waiting_user_input: bool = False + paused: bool = False + running_timer_tasks: Dict[str, asyncio.Task] = field(default_factory=dict) + input_events: List[dict] = field(default_factory=list) + output_events: List[dict] = field(default_factory=list) + output_state: Optional[State] = None session: PromptSession = PromptSession() status = console.status("[bold green]Working ...[/]") events_counter = 0 + first_time: bool = False + + +async def _run_chat_v2_x(rails_app: LLMRails): + """Simple chat loop for v2.x using the stateful events API.""" + chat_state = ChatState() def watcher(*args): - nonlocal events_counter - events_counter += 1 - status.update(f"[bold green]Working ({events_counter} events processed)...[/]") + nonlocal chat_state + chat_state.events_counter += 1 + chat_state.status.update( + f"[bold green]Working ({chat_state.events_counter} events processed)...[/]" + ) rails_app.runtime.watchers.append(watcher) # Set the runtime for the debugger to work correctly. + debugger.set_chat_state(chat_state) debugger.set_runtime(cast(RuntimeV2_x, rails_app.runtime)) # Start an asynchronous timer async def _start_timer(timer_name: str, delay_seconds: float, action_uid: str): - nonlocal input_events + nonlocal chat_state await asyncio.sleep(delay_seconds) - input_events.append( + chat_state.input_events.append( new_event_dict( "TimerBotActionFinished", action_uid=action_uid, @@ -157,17 +168,22 @@ async def _start_timer(timer_name: str, delay_seconds: float, action_uid: str): timer_name=timer_name, ) ) - running_timer_tasks.pop(action_uid) - if waiting_user_input: + chat_state.running_timer_tasks.pop(action_uid) + + # Pause here until chat is resumed + while chat_state.paused: + await asyncio.sleep(0.1) + + if chat_state.waiting_user_input: await _process_input_events() def _process_output(): """Helper to process the output events.""" - nonlocal output_events, output_state, input_events, state + nonlocal chat_state # We detect any "StartUtteranceBotAction" events, show the message, and # generate the corresponding Finished events as new input events. - for event in output_events: + for event in chat_state.output_events: if event["type"] == "StartUtteranceBotAction": # We print bot messages in green. if not verbose.verbose_mode_enabled: @@ -182,13 +198,13 @@ def _process_output(): + "[/]" ) - input_events.append( + chat_state.input_events.append( new_event_dict( "UtteranceBotActionStarted", action_uid=event["action_uid"], ) ) - input_events.append( + chat_state.input_events.append( new_event_dict( "UtteranceBotActionFinished", action_uid=event["action_uid"], @@ -207,13 +223,13 @@ def _process_output(): "[black on blue]" + f"bot gesture: {event['gesture']}" + "[/]" ) - input_events.append( + chat_state.input_events.append( new_event_dict( "GestureBotActionStarted", action_uid=event["action_uid"], ) ) - input_events.append( + chat_state.input_events.append( new_event_dict( "GestureBotActionFinished", action_uid=event["action_uid"], @@ -233,7 +249,7 @@ def _process_output(): + f"bot posture (start): (posture={event['posture']}, action_uid={event['action_uid']}))" + "[/]" ) - input_events.append( + chat_state.input_events.append( new_event_dict( "PostureBotActionStarted", action_uid=event["action_uid"], @@ -248,7 +264,7 @@ def _process_output(): + "[/]" ) - input_events.append( + chat_state.input_events.append( new_event_dict( "PostureBotActionFinished", action_uid=event["action_uid"], @@ -272,7 +288,7 @@ def _process_output(): + "[/]" ) - input_events.append( + chat_state.input_events.append( new_event_dict( "VisualInformationSceneActionStarted", action_uid=event["action_uid"], @@ -287,7 +303,7 @@ def _process_output(): + "[/]" ) - input_events.append( + chat_state.input_events.append( new_event_dict( "VisualInformationSceneActionFinished", action_uid=event["action_uid"], @@ -307,7 +323,7 @@ def _process_output(): + f"scene form (start): (prompt={event['prompt']}, action_uid={event['action_uid']}, inputs={event['inputs']})" + "[/]" ) - input_events.append( + chat_state.input_events.append( new_event_dict( "VisualFormSceneActionStarted", action_uid=event["action_uid"], @@ -321,7 +337,7 @@ def _process_output(): + f"scene form (stop): (action_uid={event['action_uid']})" + "[/]" ) - input_events.append( + chat_state.input_events.append( new_event_dict( "VisualFormSceneActionFinished", action_uid=event["action_uid"], @@ -344,7 +360,7 @@ def _process_output(): + f"scene choice (start): (prompt={event['prompt']}, action_uid={event['action_uid']}, options={event['options']})" + "[/]" ) - input_events.append( + chat_state.input_events.append( new_event_dict( "VisualChoiceSceneActionStarted", action_uid=event["action_uid"], @@ -358,7 +374,7 @@ def _process_output(): + f"scene choice (stop): (action_uid={event['action_uid']})" + "[/]" ) - input_events.append( + chat_state.input_events.append( new_event_dict( "VisualChoiceSceneActionFinished", action_uid=event["action_uid"], @@ -370,10 +386,10 @@ def _process_output(): action_uid = event["action_uid"] timer = _start_timer(event["timer_name"], event["duration"], action_uid) # Manage timer tasks - if action_uid not in running_timer_tasks: + if action_uid not in chat_state.running_timer_tasks: task = asyncio.create_task(timer) - running_timer_tasks.update({action_uid: task}) - input_events.append( + chat_state.running_timer_tasks.update({action_uid: task}) + chat_state.input_events.append( new_event_dict( "TimerBotActionStarted", action_uid=event["action_uid"], @@ -382,15 +398,15 @@ def _process_output(): elif event["type"] == "StopTimerBotAction": action_uid = event["action_uid"] - if action_uid in running_timer_tasks: - running_timer_tasks[action_uid].cancel() - running_timer_tasks.pop(action_uid) + if action_uid in chat_state.running_timer_tasks: + chat_state.running_timer_tasks[action_uid].cancel() + chat_state.running_timer_tasks.pop(action_uid) elif event["type"] == "TimerBotActionFinished": action_uid = event["action_uid"] - if action_uid in running_timer_tasks: - running_timer_tasks[action_uid].cancel() - running_timer_tasks.pop(action_uid) + if action_uid in chat_state.running_timer_tasks: + chat_state.running_timer_tasks[action_uid].cancel() + chat_state.running_timer_tasks.pop(action_uid) elif event["type"].endswith("Exception"): if event["type"].endswith("Exception"): console.print("[red]" + f"Event: {event}" + "[/]") @@ -406,79 +422,85 @@ def _process_output(): # Simulate serialization for testing # data = pickle.dumps(output_state) # output_state = pickle.loads(data) - state = output_state + chat_state.state = chat_state.output_state async def _check_local_async_actions(): - nonlocal output_events, output_state, input_events, check_task + nonlocal chat_state, check_task while True: # We only run the check when we wait for user input, but not the first time. - if not waiting_user_input or first_time: + if not chat_state.waiting_user_input or chat_state.first_time: await asyncio.sleep(0.1) continue - if len(input_events) == 0: - input_events = [new_event_dict("CheckLocalAsync")] + if len(chat_state.input_events) == 0: + chat_state.input_events = [new_event_dict("CheckLocalAsync")] # We need to copy input events to prevent race condition - input_events_copy = input_events.copy() - input_events = [] - output_events, output_state = await rails_app.process_events_async( - input_events_copy, state + input_events_copy = chat_state.input_events.copy() + chat_state.input_events = [] + ( + chat_state.output_events, + chat_state.output_state, + ) = await rails_app.process_events_async( + input_events_copy, chat_state.state ) # Process output_events and potentially generate new input_events _process_output() if ( - len(output_events) == 1 - and output_events[0]["type"] == "LocalAsyncCounter" - and output_events[0]["counter"] == 0 + len(chat_state.output_events) == 1 + and chat_state.output_events[0]["type"] == "LocalAsyncCounter" + and chat_state.output_events[0]["counter"] == 0 ): # If there are no pending actions, we stop check_task.cancel() check_task = None - debugger.set_output_state(output_state) - status.stop() + debugger.set_output_state(chat_state.output_state) + chat_state.status.stop() enable_input.set() return - output_events.clear() + chat_state.output_events.clear() await asyncio.sleep(0.2) async def _process_input_events(): - nonlocal first_time, output_events, output_state, input_events, check_task - while input_events or first_time: + nonlocal chat_state, check_task + while chat_state.input_events or chat_state.first_time: # We need to copy input events to prevent race condition - input_events_copy = input_events.copy() - input_events = [] - output_events, output_state = await rails_app.process_events_async( - input_events_copy, state + input_events_copy = chat_state.input_events.copy() + chat_state.input_events = [] + ( + chat_state.output_events, + chat_state.output_state, + ) = await rails_app.process_events_async( + input_events_copy, chat_state.state ) - debugger.set_output_state(output_state) + debugger.set_output_state(chat_state.output_state) _process_output() # If we don't have a check task, we start it if check_task is None: check_task = asyncio.create_task(_check_local_async_actions()) - first_time = False + chat_state.first_time = False # Start the task for checking async actions check_task = asyncio.create_task(_check_local_async_actions()) # And go into the default listening loop. - first_time = True + chat_state.first_time = True with patch_stdout(raw=True): while True: - if first_time: - input_events = [] + if chat_state.first_time: + chat_state.input_events = [] else: - waiting_user_input = True + chat_state.waiting_user_input = True await enable_input.wait() - user_message: str = await session.prompt_async( + user_message: str = await chat_state.session.prompt_async( HTML("\n> "), style=Style.from_dict( { @@ -488,17 +510,17 @@ async def _process_input_events(): ), ) enable_input.clear() - events_counter = 0 - status.start() - waiting_user_input = False + chat_state.events_counter = 0 + chat_state.status.start() + chat_state.waiting_user_input = False if user_message == "": - input_events = [new_event_dict("CheckLocalAsync")] + chat_state.input_events = [new_event_dict("CheckLocalAsync")] # System commands elif user_message.startswith("!"): command = user_message[1:] debugger.run_command(command) - status.stop() + chat_state.status.stop() enable_input.set() continue @@ -511,9 +533,9 @@ async def _process_input_events(): "[white on red]" + f"Invalid event: {event_input}" + "[/]" ) else: - input_events = [event] + chat_state.input_events = [event] else: - input_events = [ + chat_state.input_events = [ new_event_dict( "UtteranceUserActionFinished", final_transcript=user_message, diff --git a/nemoguardrails/cli/debugger.py b/nemoguardrails/cli/debugger.py index 1237a6ca..c5e41912 100644 --- a/nemoguardrails/cli/debugger.py +++ b/nemoguardrails/cli/debugger.py @@ -37,10 +37,15 @@ app = typer.Typer(name="!!!", no_args_is_help=True, add_completion=False) +def set_chat_state(_chat_state: "ChatState"): + """Register the chat state that will be used by the debugger.""" + global chat_state + chat_state = _chat_state + + def set_runtime(_runtime: RuntimeV2_x): """Registers the runtime that will be used by the debugger.""" global runtime - runtime = _runtime @@ -50,12 +55,46 @@ def set_output_state(_state: State): state = _state -# @app.command() -# def restart(): -# """Restart the current Colang script.""" -# runtime.state = None -# runtime.input_events = [] -# runtime.first_time = True +@app.command() +def restart(): + """Restart the current Colang script.""" + chat_state.state = None + chat_state.input_events = [] + chat_state.first_time = True + + +@app.command() +def pause(): + """Pause current interaction.""" + chat_state.paused = True + + +@app.command() +def resume(): + """Pause current interaction.""" + chat_state.paused = False + + +@app.command() +def show_flow( + flow_name: str = typer.Argument(help="Name of flow or uid of a flow instance."), +): + """Shows all details about a flow or flow instance.""" + assert state + + if flow_name in state.flow_configs: + flow_config = state.flow_configs[flow_name] + console.print(flow_config) + else: + matches = [ + (uid, item) for uid, item in state.flow_states.items() if flow_name in uid + ] + if matches: + flow_instance = matches[0][1] + console.print(flow_instance.__dict__) + else: + console.print(f"Flow '{flow_name}' does not exist.") + return @app.command() @@ -68,6 +107,7 @@ def list_flows( help="Order flows by flow name, otherwise its ordered by event processing priority.", ), ): + """Shows a table with all (active) flows ordered in terms of there interaction loop priority and name.""" assert state """List the flows from the current state.""" From c7c49989df531ed2a83f8c7662baf625089f9a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Fri, 30 Aug 2024 17:55:14 +0200 Subject: [PATCH 12/14] Improve CLI debugging commands --- nemoguardrails/cli/debugger.py | 29 ++++++++++++----- .../colang/v2_x/runtime/statemachine.py | 31 ++++++++++--------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/nemoguardrails/cli/debugger.py b/nemoguardrails/cli/debugger.py index c5e41912..7b060d8b 100644 --- a/nemoguardrails/cli/debugger.py +++ b/nemoguardrails/cli/debugger.py @@ -28,7 +28,7 @@ State, ) from nemoguardrails.colang.v2_x.runtime.runtime import RuntimeV2_x -from nemoguardrails.colang.v2_x.runtime.statemachine import _is_active_flow +from nemoguardrails.colang.v2_x.runtime.statemachine import is_active_flow from nemoguardrails.utils import console runtime: Optional[RuntimeV2_x] = None @@ -76,7 +76,7 @@ def resume(): @app.command() -def show_flow( +def flow( flow_name: str = typer.Argument(help="Name of flow or uid of a flow instance."), ): """Shows all details about a flow or flow instance.""" @@ -98,7 +98,7 @@ def show_flow( @app.command() -def list_flows( +def flows( all: bool = typer.Option( default=False, help="Show all flows (including inactive)." ), @@ -141,7 +141,7 @@ def get_loop_info(flow_config: FlowConfig) -> str: active_instances = [] if flow_id in state.flow_id_states: for flow_instance in state.flow_id_states[flow_id]: - if _is_active_flow(flow_instance): + if is_active_flow(flow_instance): active_instances.append(flow_instance.uid.split(")")[1][:5]) if active_instances: rows.append( @@ -179,7 +179,11 @@ def get_loop_info(flow_config: FlowConfig) -> str: @app.command() -def tree(): +def tree( + all: bool = typer.Option( + default=False, help="Show all flow instances (including inactive)." + ) +): """Lists the tree of all active flows.""" main_flow = state.flow_id_states["main"][0] @@ -194,8 +198,17 @@ def tree(): elements = flow_config.elements for child_uid in flow_state.child_flow_uids: + child_flow_config = state.flow_configs[state.flow_states[child_uid].flow_id] child_flow_state = state.flow_states[child_uid] + + if not all and not is_active_flow(child_flow_state): + continue + child_uid_short = child_uid.split(")")[1][0:3] + "..." + parameter_values = "" + for param in child_flow_config.parameters: + value = child_flow_state.context[param.name] + parameter_values += f" `{value}`" # We also want to figure out if the flow is actually waiting on this child waiting_on = False @@ -215,10 +228,12 @@ def tree(): child_flow_label = ( ("[green]>[/] " if waiting_on else "") + child_flow_state.flow_id - + " " + + parameter_values + + " (" + child_uid_short - + " " + + " ," + child_flow_state.status.value + + ")" ) child_node = node.add(child_flow_label) diff --git a/nemoguardrails/colang/v2_x/runtime/statemachine.py b/nemoguardrails/colang/v2_x/runtime/statemachine.py index d9f2a9e8..9b79e6b9 100644 --- a/nemoguardrails/colang/v2_x/runtime/statemachine.py +++ b/nemoguardrails/colang/v2_x/runtime/statemachine.py @@ -296,7 +296,7 @@ def run_to_completion(state: State, external_event: Union[dict, Event]) -> State # Find all active interaction loops active_interaction_loops = set() for flow_state in state.flow_states.values(): - if _is_listening_flow(flow_state): + if is_listening_flow(flow_state): active_interaction_loops.add(flow_state.loop_id) # TODO: Check if we should rather should do this after the event matching step @@ -421,7 +421,7 @@ def run_to_completion(state: State, external_event: Union[dict, Event]) -> State actionable_heads = [ head for head in actionable_heads - if _is_active_flow(get_flow_state_from_head(state, head)) + if is_active_flow(get_flow_state_from_head(state, head)) and head.status == FlowHeadStatus.ACTIVE ] @@ -546,7 +546,7 @@ def _process_internal_events_without_default_matchers( flow_instance_uid = event.arguments["flow_instance_uid"] if flow_instance_uid in state.flow_states: flow_state = state.flow_states[event.arguments["flow_instance_uid"]] - if not _is_inactive_flow(flow_state): + if not is_inactive_flow(flow_state): _finish_flow( state, flow_state, @@ -578,7 +578,7 @@ def _process_internal_events_without_default_matchers( flow_instance_uid = event.arguments["flow_instance_uid"] if flow_instance_uid in state.flow_states: flow_state = state.flow_states[flow_instance_uid] - if not _is_inactive_flow(flow_state): + if not is_inactive_flow(flow_state): _abort_flow( state=state, flow_state=flow_state, @@ -881,7 +881,7 @@ def _advance_head_front(state: State, heads: List[FlowHead]) -> List[FlowHead]: flow_state = get_flow_state_from_head(state, head) flow_config = get_flow_config_from_head(state, head) - if head.status == FlowHeadStatus.INACTIVE or not _is_listening_flow(flow_state): + if head.status == FlowHeadStatus.INACTIVE or not is_listening_flow(flow_state): continue elif head.status == FlowHeadStatus.MERGING and len(state.internal_events) > 0: # We only advance merging heads if all internal events were processed @@ -1340,7 +1340,7 @@ def slide( # TODO: This should not be needed if states would be cleaned-up correctly if flow_uid in state.flow_states: child_flow_state = state.flow_states[flow_uid] - if _is_listening_flow(child_flow_state): + if is_listening_flow(child_flow_state): _abort_flow(state, child_flow_state, head.matching_scores) for action_uid in action_uids: action = state.actions[action_uid] @@ -1440,7 +1440,7 @@ def _abort_flow( else: return - if not _is_listening_flow(flow_state) and flow_state.status != FlowStatus.STOPPING: + if not is_listening_flow(flow_state) and flow_state.status != FlowStatus.STOPPING: # Skip the rest for all inactive flows return @@ -1534,7 +1534,7 @@ def _finish_flow( else: return - if not _is_listening_flow(flow_state): + if not is_listening_flow(flow_state): # Skip the rest for all inactive flows return @@ -1745,7 +1745,7 @@ def _flow_head_changed(state: State, flow_state: FlowState, head: FlowHead) -> N if ( element is not None and head.status is not FlowHeadStatus.INACTIVE - and _is_listening_flow(flow_state) + and is_listening_flow(flow_state) and is_match_op_element(element) ): _add_head_to_event_matching_structures(state, flow_state, head) @@ -1785,7 +1785,7 @@ def _remove_head_from_event_matching_structures( def _update_action_status_by_event(state: State, event: ActionEvent) -> None: for flow_state in state.flow_states.values(): - if not _is_listening_flow(flow_state): + if not is_listening_flow(flow_state): # Don't process flows that are not active continue @@ -1797,7 +1797,8 @@ def _update_action_status_by_event(state: State, event: ActionEvent) -> None: action.process_event(event) -def _is_listening_flow(flow_state: FlowState) -> bool: +def is_listening_flow(flow_state: FlowState) -> bool: + """True if flow is started or waiting to be started.""" return ( flow_state.status == FlowStatus.WAITING or flow_state.status == FlowStatus.STARTED @@ -1805,14 +1806,16 @@ def _is_listening_flow(flow_state: FlowState) -> bool: ) -def _is_active_flow(flow_state: FlowState) -> bool: +def is_active_flow(flow_state: FlowState) -> bool: + """True if flow has started.""" return ( flow_state.status == FlowStatus.STARTED or flow_state.status == FlowStatus.STARTING ) -def _is_inactive_flow(flow_state: FlowState) -> bool: +def is_inactive_flow(flow_state: FlowState) -> bool: + """True if flow is not started.""" return ( flow_state.status == FlowStatus.WAITING or flow_state.status == FlowStatus.STOPPED @@ -2045,7 +2048,7 @@ def find_all_active_event_matchers( """Return a list of all active heads that point to an event 'match' element.""" event_matchers: List[FlowHead] = [] for flow_state in state.flow_states.values(): - if not _is_active_flow(flow_state) or not _is_listening_flow(flow_state): + if not is_active_flow(flow_state) or not is_listening_flow(flow_state): continue flow_config = state.flow_configs[flow_state.flow_id] From 01ebd3a2b2942c83e2dec1dcf25713495134598f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Fri, 30 Aug 2024 18:09:50 +0200 Subject: [PATCH 13/14] Update documentation --- .../development-and-debugging.rst | 120 +++++++++++++----- 1 file changed, 88 insertions(+), 32 deletions(-) diff --git a/docs/colang_2/language_reference/development-and-debugging.rst b/docs/colang_2/language_reference/development-and-debugging.rst index 06c40cc0..688ebe1a 100644 --- a/docs/colang_2/language_reference/development-and-debugging.rst +++ b/docs/colang_2/language_reference/development-and-debugging.rst @@ -31,7 +31,7 @@ To help debugging your Colang flows you can use the print statement ``print Hi @@ -75,44 +75,100 @@ Where ``pretty_str`` converts the returned dictionary object to a nicely formatt CLI Debugging Commands ------------------------- -The NeMo Guardrail CLI provides a couple of additional debugging commands that always start with the ``!`` character, e.g.: +The NeMo Guardrail CLI provides a couple of additional debugging commands that always start with the ``!`` character: -.. code-block:: text +.. code-block:: console > !list-flows - ┏━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ - ┃ ID ┃ Flow Name ┃ Loop ┃ Flow Instances ┃ Source ┃ - ┡━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ - │ 1 │ tracking visual choice selection state │ named ('state_tracking') │ 924ad │ /colang/v2_x/library/avatars.co │ - │ 2 │ tracking bot talking state │ named ('state_tracking') │ 28804 │ /colang/v2_x/library/core.co │ - │ 3 │ wait │ new │ 9e18b │ /colang/v2_x/library/timing.co │ - │ 4 │ user was silent │ named ('user_was_silent') │ c1dfd │ /colang/v2_x/library/timing.co │ - │ 5 │ polling llm request response │ named ('llm_response_polling') │ a003b │ /colang/v2_x/library/llm.co │ - │ 6 │ continuation on unhandled user utterance │ parent │ 342c6 │ /colang/v2_x/library/llm.co │ - │ 7 │ automating intent detection │ parent │ c5c0a │ /colang/v2_x/library/llm.co │ - │ 8 │ marking user intent flows │ named ('intent_log') │ ad2b4 │ /colang/v2_x/library/llm.co │ - │ 9 │ logging marked user intent flows │ named ('intent_log') │ a5dd9 │ /colang/v2_x/library/llm.co │ - │ 10 │ marking bot intent flows │ named ('intent_log') │ 8873e │ /colang/v2_x/library/llm.co │ - │ 11 │ logging marked bot intent flows │ named ('intent_log') │ 3d331 │ /colang/v2_x/library/llm.co │ - │ 12 │ user has selected choice │ parent │ 78b3b,783b6,c4186,cd667 │ /colang/v2_x/library/avatars.co │ - │ 13 │ user interrupted bot talking │ parent │ 6e576 │ /colang/v2_x/library/avatars.co │ - │ 14 │ bot posture │ parent │ d9f32 │ /colang/v2_x/library/avatars.co │ - │ 15 │ handling bot talking interruption │ named ('bot_interruption') │ 625f1 │ /colang/v2_x/library/avatars.co │ - │ 16 │ managing idle posture │ named ('managing_idle_posture') │ 0bfe3 │ /colang/v2_x/library/avatars.co │ - │ 17 │ _user_said │ parent │ db5e4,d2cb3,b7b85,095e0 │ /colang/v2_x/library/core.co │ - │ 18 │ _user_said_something_unexpected │ parent │ cb676 │ /colang/v2_x/library/core.co │ - │ 19 │ user said │ parent │ c4a05,45f2c,cd4ab,fecc2 │ /colang/v2_x/library/core.co │ - │ 20 │ bot started saying something │ parent │ fc2a7,8d5f1 │ /colang/v2_x/library/core.co │ - │ 21 │ notification of colang errors │ named ('colang_errors_warning') │ cd5a8 │ /colang/v2_x/library/core.co │ - │ 22 │ notification of undefined flow start │ parent │ 20d10 │ /colang/v2_x/library/core.co │ - │ 23 │ wait indefinitely │ parent │ 3713b │ /colang/v2_x/library/core.co │ - └────┴───────────────────────────────────────────┴─────────────────────────────────┴─────────────────────────┴─────────────────────────────────┘ + ┏━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ ID ┃ Flow Name ┃ Loop (Priority | Type | Id) ┃ Flow Instances ┃ Source ┃ + ┡━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ + │ 1 │ tracking bot talking state │ 10 │ Named │ 'state_tracking' │ 4d0cb,3ea17,b3b49 │ /colang/v2_x/library/core.co │ + │ 2 │ tracking user talking state │ 10 │ Named │ 'state_tracking' │ │ /colang/v2_x/library/core.co │ + │ 3 │ tracking visual choice selection state │ 10 │ Named │ 'state_tracking' │ fa0f8,196ad │ /colang/v2_x/library/avatars.co │ + │ 4 │ _bot_say │ 0 │ parent │ b6036 │ /colang/v2_x/library/core.co │ + │ 5 │ _user_said │ 0 │ parent │ 303c3,565f7,cca80 │ /colang/v2_x/library/core.co │ + │ 6 │ _user_said_something_unexpected │ 0 │ parent │ 1b5f7,41f02 │ /colang/v2_x/library/core.co │ + │ 7 │ _user_saying │ 0 │ parent │ a65e9 │ /colang/v2_x/library/core.co │ + │ 8 │ automating intent detection │ 0 │ parent │ c7ead │ /colang/v2_x/library/llm.co │ + │ 9 │ await_flow_by_name │ 0 │ parent │ 5f7ef │ /colang/v2_x/library/core.co │ + │ 10 │ bot answer question about france │ 0 │ parent │ │ llm_example_flows.co │ + │ 11 │ bot ask │ 0 │ parent │ │ /colang/v2_x/library/core.co │ + │ 12 │ bot ask how are you │ 0 │ parent │ │ demo.co │ + │ 13 │ bot ask user for age │ 0 │ parent │ │ llm_example_flows.co │ + │ 14 │ bot ask user to pick a color │ 0 │ parent │ │ llm_example_flows.co │ + │ 15 │ bot asked something │ 0 │ parent │ │ /colang/v2_x/library/core.co │ + │ 16 │ bot asks email address │ 0 │ parent │ │ show_case_back_channelling_interaction.co │ + │ 17 │ bot asks user how the day was │ 0 │ parent │ │ show_case_back_channelling_interaction.co │ + │ 18 │ bot attract user │ 0 │ parent │ │ llm_example_flows.co │ + │ 19 │ bot clarified something │ 0 │ parent │ │ /colang/v2_x/library/core.co │ + │ 20 │ bot clarify │ 0 │ parent │ │ /colang/v2_x/library/core.co │ + │ 21 │ bot count from a number to another number │ 0 │ parent │ │ llm_example_flows.co │ + │ 22 │ bot express │ 0 │ parent │ │ /colang/v2_x/library/core.co │ + │ 23 │ bot express feeling bad │ 0 │ parent │ │ demo.co │ + │ 24 │ bot express feeling well │ 0 │ parent │ │ demo.co │ + │ 25 │ bot express greeting │ 0 │ parent │ │ demo.co │ + │ 26 │ bot expressed something │ 0 │ parent │ │ /colang/v2_x/library/core.co │ + │ 27 │ bot gesture │ 0 │ parent │ a2528 │ /colang/v2_x/library/avatars.co │ + │ 28 │ bot gesture with delay │ 0 │ parent │ │ /colang/v2_x/library/avatars.co │ + │ 29 │ bot inform │ 0 │ parent │ da186 │ /colang/v2_x/library/core.co │ + │ 30 │ bot inform about service │ 0 │ parent │ │ demo.co │ + │ 31 │ bot informed something │ 0 │ parent │ │ /colang/v2_x/library/core.co │ + │ 32 │ bot make long pause │ 0 │ parent │ │ demo.co │ + │ 33 │ bot make short pause │ 0 │ parent │ │ demo.co │ + └────┴───────────────────────────────────────────┴───┴───────────────────────────┴───────────────────┴───────────────────────────────────────────┴ + +.. code-block:: console + + > !tree + main + ├── notification of undefined flow start `Excuse me, what did you say?` (fdb... ,started) + ├── notification of colang errors `Excuse me, what did you say?` (69c... ,started) + ├── automating intent detection (c7e... ,started) + │ ├── logging marked user intent flows (d84... ,started) + │ └── logging marked bot intent flows (462... ,started) + ├── showcase selector (742... ,started) + │ ├── handling bot talking interruption `inform` `stop|cancel` (dea... ,started) + │ │ └── > user interrupted bot talking `15` `stop|cancel` (109... ,started) + │ │ └── > bot started saying something (561... ,started) + │ ├── > user was silent `15.0` (40f... ,started) + │ │ └── > wait `15.0` `wait_timer_1f499f52-9634-4925-b18a-579fef485d5e` (ae4... ,started) + │ ├── > user picked number guessing game showcase (6bc... ,started) + │ │ ├── > user has selected choice `game` (b88... ,started) + │ │ ├── > user said `I want to play the number guessing game` (448... ,started) + │ │ │ └── > _user_said `I want to play the number guessing game` (303... ,started) + │ │ ├── > user said `Show me the game` (d32... ,started) + │ │ │ └── > _user_said `Show me the game` (565... ,started) + │ │ ├── > user said `showcase A` (3fd... ,started) + │ │ │ └── > _user_said `showcase A` (cca... ,started) + │ │ ├── > user said `First showcase` (013... ,started) + │ │ │ └── > _user_said `First showcase` (9d4... ,started) + │ │ └── > user said `re.compile('(?i)guessing game', re.IGNORECASE)` (af9... ,started) + │ │ └── > _user_said `re.compile('(?i)guessing game', re.IGNORECASE)` (966... ,started) + │ ├── > user picked multimodality showcase (e2d... ,started) + │ │ ├── > user has selected choice `multimodality` (f69... ,started) + │ │ ├── > user said `Show me the multimodality showcase` (9be... ,started) + │ │ │ └── > _user_said `Show me the multimodality showcase` (33f... ,started) + │ │ ├── > user said `multimodality` (dfe... ,started) + │ │ │ └── > _user_said `multimodality` (18f... ,started) + │ │ ├── > user said `showcase B` (aa6... ,started) + │ │ │ └── > _user_said `showcase B` (4c5... ,started) + │ │ ├── > user said `Second showcase` (205... ,started) + │ │ │ └── > _user_said `Second showcase` (92a... ,started) + │ │ └── > user said `re.compile('(?i)multimodality', re.IGNORECASE)` (b10... ,started) + │ │ └── > _user_said `re.compile('(?i)multimodality', re.IGNORECASE)` (d86... ,started) + .. code-block:: colang :caption: All CLI debugging commands - list-flows [--all] [--order_by_name] # Shows all active flows in a table in order of their interaction loop priority and name - tree # Shows the flow hierarchy tree of all active flows + flows [--all] [--order_by_name] # Shows all (active) flows in a table in order of their interaction loop priority and name + tree # Shows the flow hierarchy tree of all (active) flows + flow [|] # Show flow or flow instance details + pause # Pause timer event processing such that interaction does not continue on its own + resume # Resume timer event processing, including the ones trigger during paus + restart # Reset interaction and restart the Colang script + ------------- Useful Flows From 51c0179b404ea6edc7c2267800e30398747c0498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Sch=C3=BCller?= Date: Fri, 30 Aug 2024 18:12:01 +0200 Subject: [PATCH 14/14] Update Colang changelog --- CHANGELOG-Colang.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-Colang.md b/CHANGELOG-Colang.md index 49c24102..7319e37b 100644 --- a/CHANGELOG-Colang.md +++ b/CHANGELOG-Colang.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * [#703](https://github.com/NVIDIA/NeMo-Guardrails/pull/703) Add bot configuration as variable `$system.config`. * [#709](https://github.com/NVIDIA/NeMo-Guardrails/pull/709) Add basic support for most OpenAI and LLame 3 models. * [#712](https://github.com/NVIDIA/NeMo-Guardrails/pull/712) Add interaction loop priority levels for flows. +* [#717](https://github.com/NVIDIA/NeMo-Guardrails/pull/717) Add CLI chat debugging commands. ### Changed