Skip to content

Fix/multiline string expressions #579

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
Expand Down
2 changes: 1 addition & 1 deletion examples/v2_x/tutorial/guardrails_1/rails.co
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 7 additions & 3 deletions nemoguardrails/colang/v2_x/lang/expansion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions nemoguardrails/colang/v2_x/lang/grammar/colang.lark
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ _NEWLINE: (/\r?\n[\t ]*/)+

// Primitive values

STRING: /i?("(?!"").*?(?<!\\)(\\\\)*?"|i?'(?!'').*?(?<!\\)(\\\\)*?')/i
LONG_STRING: /(""".*?(?<!\\)(\\\\)*?"""|'''.*?(?<!\\)(\\\\)*?''')/is
STRING: /(\.\.\.\s*)?("(?!"").*?(?<!\\)(\\\\)*?"|(\.\.\.\s*)?'(?!'').*?(?<!\\)(\\\\)*?')/i
LONG_STRING: /(\.\.\.\s*)?(""".*?(?<!\\)(\\\\)*?"""|(\.\.\.\s*)?'''.*?(?<!\\)(\\\\)*?''')/is

_SPECIAL_DEC: "0".."9" ("_"? "0".."9" )*
DEC_NUMBER: "1".."9" ("_"? "0".."9" )*
Expand Down
3 changes: 0 additions & 3 deletions nemoguardrails/colang/v2_x/lang/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@ def _apply_pre_parsing_expansions(content: str):

Currently, only the "..." is expanded.
"""
# Replace ..."NLD" with i"NLD"
content = re.sub(r"\.\.\.(['\"])(.*?)\1", r'i"\2"', content)

# We make sure to capture the correct indentation level and use that.
content = re.sub(
r"\n( +)\.\.\.",
Expand Down
23 changes: 17 additions & 6 deletions nemoguardrails/colang/v2_x/runtime/eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
from nemoguardrails.colang.v2_x.runtime import system_functions
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.colang.v2_x.runtime.utils import (
AttributeDict,
escape_special_string_characters,
)
from nemoguardrails.eval.cli.simplify_formatter import SimplifyFormatter
from nemoguardrails.utils import new_uid

Expand Down Expand Up @@ -67,14 +70,17 @@ def eval_expression(expr: str, context: dict) -> 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
Expand All @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions nemoguardrails/colang/v2_x/runtime/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import re
import uuid


Expand Down Expand Up @@ -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
65 changes: 65 additions & 0 deletions tests/v2_x/test_llm_value_generation.py
Original file line number Diff line number Diff line change
@@ -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()
37 changes: 32 additions & 5 deletions tests/v2_x/test_slide_mechanics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -241,20 +244,44 @@ def test_expressions_in_strings():
[
{
"type": "StartUtteranceBotAction",
"script": "Roger",
"script": "Roger's\n test:\n x'y'z\n",
},
{
"type": "StartUtteranceBotAction",
"script": "It's {->}",
},
{
"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."""

Expand Down Expand Up @@ -950,4 +977,4 @@ def test_expression_evaluation():


if __name__ == "__main__":
test_expression_evaluation()
test_multiline_string()