diff --git a/examples/v2_x/language_reference/use_llms/interaction_loop/main.co b/examples/v2_x/language_reference/use_llms/interaction_loop/main.co index df61adf2a..623514979 100644 --- a/examples/v2_x/language_reference/use_llms/interaction_loop/main.co +++ b/examples/v2_x/language_reference/use_llms/interaction_loop/main.co @@ -10,7 +10,7 @@ flow main when unhandled user intent llm continue interaction or when user was silent 12.0 - $response = i"A random fun fact" + $response = ..."A random fun fact" bot say $response or when user expressed greeting bot say "Hi there!" diff --git a/examples/v2_x/tutorial/guardrails_1/rails.co b/examples/v2_x/tutorial/guardrails_1/rails.co index c4cde3018..24dd8a087 100644 --- a/examples/v2_x/tutorial/guardrails_1/rails.co +++ b/examples/v2_x/tutorial/guardrails_1/rails.co @@ -24,6 +24,6 @@ flow input rails $input_text abort flow check user utterance $input_text -> $input_safe - $is_safe = i"Consider the following user utterance: '{$input_text}'. Assign 'True' if appropriate, 'False' if inappropriate." + $is_safe = ..."Consider the following user utterance: '{$input_text}'. Assign 'True' if appropriate, 'False' if inappropriate." print $is_safe return $is_safe diff --git a/nemoguardrails/colang/v2_x/lang/expansion.py b/nemoguardrails/colang/v2_x/lang/expansion.py index 5e811a6bd..847031eb1 100644 --- a/nemoguardrails/colang/v2_x/lang/expansion.py +++ b/nemoguardrails/colang/v2_x/lang/expansion.py @@ -41,7 +41,10 @@ ) 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 +from nemoguardrails.colang.v2_x.runtime.utils import ( + escape_special_string_characters, + new_var_uid, +) def expand_elements( @@ -653,11 +656,12 @@ def _expand_assignment_stmt_element(element: Assignment) -> List[ElementType]: new_elements: List[ElementType] = [] # Check if the expression is an NLD instruction - nld_instruction_pattern = r"^\s*i\"(.*)\"|^\s*i'(.*)'" + nld_instruction_pattern = r'\.\.\.\s*("""|\'\'\')((?:\\\1|(?!\1)[\s\S])*?)\1|\.\.\.\s*("|\')((?:\\\3|(?!\3).)*?)\3' match = re.search(nld_instruction_pattern, element.expression) if match: # Replace the assignment with the GenerateValueAction system action + instruction = escape_special_string_characters(match.group(2) or match.group(4)) new_elements.append( SpecOp( op="await", @@ -666,7 +670,7 @@ def _expand_assignment_stmt_element(element: Assignment) -> List[ElementType]: spec_type=SpecType.ACTION, arguments={ "var_name": f'"{element.key}"', - "instructions": f'"{match.group(1) or match.group(2)}"', + "instructions": f'"{instruction}"', }, ), return_var_name=element.key, diff --git a/nemoguardrails/colang/v2_x/lang/grammar/colang.lark b/nemoguardrails/colang/v2_x/lang/grammar/colang.lark index 5d848d505..8b8980466 100644 --- a/nemoguardrails/colang/v2_x/lang/grammar/colang.lark +++ b/nemoguardrails/colang/v2_x/lang/grammar/colang.lark @@ -221,8 +221,8 @@ _NEWLINE: (/\r?\n[\t ]*/)+ // Primitive values -STRING: /i?("(?!"").*?(? Any: # We search for all expressions in strings within curly brackets and evaluate them first # Find first all strings - string_pattern = r'("(?:\\"|[^"])*?")|(\'(?:\\\'|[^\'])*?\')' + string_pattern = ( + r'("""|\'\'\')((?:\\\1|(?!\1)[\s\S])*?)\1|("|\')((?:\\\3|(?!\3).)*?)\3' + ) string_expressions_matches = re.findall(string_pattern, expr) string_expression_values = [] for string_expression_match in string_expressions_matches: + character = string_expression_match[0] or string_expression_match[2] string_expression = ( - string_expression_match[0] - if string_expression_match[0] - else string_expression_match[1] + character + + (string_expression_match[1] or string_expression_match[3]) + + character ) if string_expression: # Find expressions within curly brackets, ignoring double curly brackets @@ -89,7 +95,12 @@ def eval_expression(expr: str, context: dict) -> Any: raise ColangValueError( f"Error evaluating inner expression: '{inner_expression}'" ) from ex - value = str(value).replace('"', '\\"').replace("'", "\\'") + + value = str(value) + + # Escape special characters + value = escape_special_string_characters(value) + inner_expression_values.append(value) string_expression = re.sub( expression_pattern, diff --git a/nemoguardrails/colang/v2_x/runtime/utils.py b/nemoguardrails/colang/v2_x/runtime/utils.py index e623d3733..ed279e080 100644 --- a/nemoguardrails/colang/v2_x/runtime/utils.py +++ b/nemoguardrails/colang/v2_x/runtime/utils.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re import uuid @@ -40,3 +41,23 @@ 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 escape_special_string_characters(string: str) -> str: + """Escapes all occurrences of special characters.""" + # Replace " or ' with \\" or \\' if not already escaped + string = re.sub(r"(^|[^\\])('|\")", r"\1\\\2", string) + # Replace other special characters + escaped_characters_map = { + "\n": "\\n", + "\t": "\\t", + "\r": "\\r", + "\b": "\\b", + "\f": "\\f", + "\v": "\\v", + } + + for c, s in escaped_characters_map.items(): + string = str(string).replace(c, s) + + return string diff --git a/tests/v2_x/test_llm_value_generation.py b/tests/v2_x/test_llm_value_generation.py new file mode 100644 index 000000000..5ab371a77 --- /dev/null +++ b/tests/v2_x/test_llm_value_generation.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from rich.logging import RichHandler + +from nemoguardrails import RailsConfig +from tests.utils import TestChat + +FORMAT = "%(message)s" +logging.basicConfig( + level=logging.DEBUG, + format=FORMAT, + datefmt="[%X,%f]", + handlers=[RichHandler(markup=True)], +) + + +def test_1(): + """Test use of expression as statements.""" + config = RailsConfig.from_content( + colang_content=""" + flow main + match UtteranceUserActionFinished() + $v1 = ..."Generate the company' name\\" from users input" + $v2 = ... 'Generate the company\\' name" from users input' + $v3 = ...'''Generate the company' name" from users input''' + $v4 = ... '''Generate the company' + name" from users input''' + + await UtteranceBotAction(script="{$v1}{$v2}{$v3}{$v4}") + """, + yaml_content=""" + colang_version: "2.x" + models: + - type: main + engine: openai + model: gpt-3.5-turbo-instruct + """, + ) + + chat = TestChat( + config, + llm_completions=["'1'", "'2'", "'3'", "'4'"], + ) + + chat >> "hi" + chat << "1234" + + +if __name__ == "__main__": + test_1() diff --git a/tests/v2_x/test_slide_mechanics.py b/tests/v2_x/test_slide_mechanics.py index 287488ecd..4fd4249f8 100644 --- a/tests/v2_x/test_slide_mechanics.py +++ b/tests/v2_x/test_slide_mechanics.py @@ -229,9 +229,12 @@ def test_expressions_in_strings(): content = """ flow main - start UtteranceBotAction(script="Roger") as $ref + $xyz = 'x\\'y\\'z\\n' + $test = '''Roger's + test:\n {$xyz}''' + start UtteranceBotAction(script=$test) as $ref start UtteranceBotAction(script="It's {{->}}") - start UtteranceBotAction(script='It"s {{->}} \\'{$ref.start_event_arguments.script}!\\'') + await UtteranceBotAction(script='It"s {{->}} \\'{$ref.start_event_arguments.script}!\\'') """ config = _init_state(content) @@ -241,7 +244,7 @@ def test_expressions_in_strings(): [ { "type": "StartUtteranceBotAction", - "script": "Roger", + "script": "Roger's\n test:\n x'y'z\n", }, { "type": "StartUtteranceBotAction", @@ -249,12 +252,36 @@ def test_expressions_in_strings(): }, { "type": "StartUtteranceBotAction", - "script": "It\"s {->} 'Roger!'", + "script": "It\"s {->} 'Roger's\n test:\n x'y'z\n!'", }, ], ) +def test_multiline_string(): + """Test string expression evaluation.""" + + content = ''' + flow main + $var = "Test" + $string = """This is a multiline + string! '{$var}' "hi" """ + start UtteranceBotAction(script=$string) + ''' + + config = _init_state(content) + state = run_to_completion(config, start_main_flow_event) + assert is_data_in_events( + state.outgoing_events, + [ + { + "type": "StartUtteranceBotAction", + "script": "This is a multiline\n string! 'Test' \"hi\" ", + } + ], + ) + + def test_flow_return_values(): """Test flow return value handling.""" @@ -950,4 +977,4 @@ def test_expression_evaluation(): if __name__ == "__main__": - test_expression_evaluation() + test_multiline_string()