Skip to content

Commit db5a65e

Browse files
authored
Merge pull request #579
Fix/multiline string expressions
2 parents c765ea0 + 8abc4c9 commit db5a65e

File tree

9 files changed

+146
-21
lines changed

9 files changed

+146
-21
lines changed

examples/v2_x/language_reference/use_llms/interaction_loop/main.co

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ flow main
1010
when unhandled user intent
1111
llm continue interaction
1212
or when user was silent 12.0
13-
$response = i"A random fun fact"
13+
$response = ..."A random fun fact"
1414
bot say $response
1515
or when user expressed greeting
1616
bot say "Hi there!"

examples/v2_x/tutorial/guardrails_1/rails.co

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ flow input rails $input_text
2424
abort
2525

2626
flow check user utterance $input_text -> $input_safe
27-
$is_safe = i"Consider the following user utterance: '{$input_text}'. Assign 'True' if appropriate, 'False' if inappropriate."
27+
$is_safe = ..."Consider the following user utterance: '{$input_text}'. Assign 'True' if appropriate, 'False' if inappropriate."
2828
print $is_safe
2929
return $is_safe

nemoguardrails/colang/v2_x/lang/expansion.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@
4141
)
4242
from nemoguardrails.colang.v2_x.runtime.errors import ColangSyntaxError
4343
from nemoguardrails.colang.v2_x.runtime.flows import FlowConfig, InternalEvents
44-
from nemoguardrails.colang.v2_x.runtime.utils import new_var_uid
44+
from nemoguardrails.colang.v2_x.runtime.utils import (
45+
escape_special_string_characters,
46+
new_var_uid,
47+
)
4548

4649

4750
def expand_elements(
@@ -676,11 +679,12 @@ def _expand_assignment_stmt_element(element: Assignment) -> List[ElementType]:
676679
new_elements: List[ElementType] = []
677680

678681
# Check if the expression is an NLD instruction
679-
nld_instruction_pattern = r"^\s*i\"(.*)\"|^\s*i'(.*)'"
682+
nld_instruction_pattern = r'\.\.\.\s*("""|\'\'\')((?:\\\1|(?!\1)[\s\S])*?)\1|\.\.\.\s*("|\')((?:\\\3|(?!\3).)*?)\3'
680683
match = re.search(nld_instruction_pattern, element.expression)
681684

682685
if match:
683686
# Replace the assignment with the GenerateValueAction system action
687+
instruction = escape_special_string_characters(match.group(2) or match.group(4))
684688
new_elements.append(
685689
SpecOp(
686690
op="await",
@@ -689,7 +693,7 @@ def _expand_assignment_stmt_element(element: Assignment) -> List[ElementType]:
689693
spec_type=SpecType.ACTION,
690694
arguments={
691695
"var_name": f'"{element.key}"',
692-
"instructions": f'"{match.group(1) or match.group(2)}"',
696+
"instructions": f'"{instruction}"',
693697
},
694698
),
695699
return_var_name=element.key,

nemoguardrails/colang/v2_x/lang/grammar/colang.lark

+2-2
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,8 @@ _NEWLINE: (/\r?\n[\t ]*/)+
222222

223223
// Primitive values
224224

225-
STRING: /i?("(?!"").*?(?<!\\)(\\\\)*?"|i?'(?!'').*?(?<!\\)(\\\\)*?')/i
226-
LONG_STRING: /(""".*?(?<!\\)(\\\\)*?"""|'''.*?(?<!\\)(\\\\)*?''')/is
225+
STRING: /(\.\.\.\s*)?("(?!"").*?(?<!\\)(\\\\)*?"|(\.\.\.\s*)?'(?!'').*?(?<!\\)(\\\\)*?')/i
226+
LONG_STRING: /(\.\.\.\s*)?(""".*?(?<!\\)(\\\\)*?"""|(\.\.\.\s*)?'''.*?(?<!\\)(\\\\)*?''')/is
227227

228228
_SPECIAL_DEC: "0".."9" ("_"? "0".."9" )*
229229
DEC_NUMBER: "1".."9" ("_"? "0".."9" )*

nemoguardrails/colang/v2_x/lang/parser.py

-3
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@ def _apply_pre_parsing_expansions(content: str):
5959
6060
Currently, only the "..." is expanded.
6161
"""
62-
# Replace ..."NLD" with i"NLD"
63-
content = re.sub(r"\.\.\.(['\"])(.*?)\1", r'i"\2"', content)
64-
6562
# We make sure to capture the correct indentation level and use that.
6663
content = re.sub(
6764
r"\n( +)\.\.\.",

nemoguardrails/colang/v2_x/runtime/eval.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
from nemoguardrails.colang.v2_x.runtime import system_functions
2727
from nemoguardrails.colang.v2_x.runtime.errors import ColangValueError
2828
from nemoguardrails.colang.v2_x.runtime.flows import FlowState, State
29-
from nemoguardrails.colang.v2_x.runtime.utils import AttributeDict
29+
from nemoguardrails.colang.v2_x.runtime.utils import (
30+
AttributeDict,
31+
escape_special_string_characters,
32+
)
3033
from nemoguardrails.eval.cli.simplify_formatter import SimplifyFormatter
3134
from nemoguardrails.utils import new_uuid
3235

@@ -67,14 +70,17 @@ def eval_expression(expr: str, context: dict) -> Any:
6770

6871
# We search for all expressions in strings within curly brackets and evaluate them first
6972
# Find first all strings
70-
string_pattern = r'("(?:\\"|[^"])*?")|(\'(?:\\\'|[^\'])*?\')'
73+
string_pattern = (
74+
r'("""|\'\'\')((?:\\\1|(?!\1)[\s\S])*?)\1|("|\')((?:\\\3|(?!\3).)*?)\3'
75+
)
7176
string_expressions_matches = re.findall(string_pattern, expr)
7277
string_expression_values = []
7378
for string_expression_match in string_expressions_matches:
79+
character = string_expression_match[0] or string_expression_match[2]
7480
string_expression = (
75-
string_expression_match[0]
76-
if string_expression_match[0]
77-
else string_expression_match[1]
81+
character
82+
+ (string_expression_match[1] or string_expression_match[3])
83+
+ character
7884
)
7985
if string_expression:
8086
# Find expressions within curly brackets, ignoring double curly brackets
@@ -89,7 +95,12 @@ def eval_expression(expr: str, context: dict) -> Any:
8995
raise ColangValueError(
9096
f"Error evaluating inner expression: '{inner_expression}'"
9197
) from ex
92-
value = str(value).replace('"', '\\"').replace("'", "\\'")
98+
99+
value = str(value)
100+
101+
# Escape special characters
102+
value = escape_special_string_characters(value)
103+
93104
inner_expression_values.append(value)
94105
string_expression = re.sub(
95106
expression_pattern,

nemoguardrails/colang/v2_x/runtime/utils.py

+21
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
import re
1617
import uuid
1718

1819

@@ -40,3 +41,23 @@ def new_readable_uid(name: str) -> str:
4041
def new_var_uid() -> str:
4142
"""Creates a new uuid that is compatible with variable names."""
4243
return str(uuid.uuid4()).replace("-", "_")
44+
45+
46+
def escape_special_string_characters(string: str) -> str:
47+
"""Escapes all occurrences of special characters."""
48+
# Replace " or ' with \\" or \\' if not already escaped
49+
string = re.sub(r"(^|[^\\])('|\")", r"\1\\\2", string)
50+
# Replace other special characters
51+
escaped_characters_map = {
52+
"\n": "\\n",
53+
"\t": "\\t",
54+
"\r": "\\r",
55+
"\b": "\\b",
56+
"\f": "\\f",
57+
"\v": "\\v",
58+
}
59+
60+
for c, s in escaped_characters_map.items():
61+
string = str(string).replace(c, s)
62+
63+
return string
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import logging
17+
18+
from rich.logging import RichHandler
19+
20+
from nemoguardrails import RailsConfig
21+
from tests.utils import TestChat
22+
23+
FORMAT = "%(message)s"
24+
logging.basicConfig(
25+
level=logging.DEBUG,
26+
format=FORMAT,
27+
datefmt="[%X,%f]",
28+
handlers=[RichHandler(markup=True)],
29+
)
30+
31+
32+
def test_1():
33+
"""Test use of expression as statements."""
34+
config = RailsConfig.from_content(
35+
colang_content="""
36+
flow main
37+
match UtteranceUserActionFinished()
38+
$v1 = ..."Generate the company' name\\" from users input"
39+
$v2 = ... 'Generate the company\\' name" from users input'
40+
$v3 = ...'''Generate the company' name" from users input'''
41+
$v4 = ... '''Generate the company'
42+
name" from users input'''
43+
44+
await UtteranceBotAction(script="{$v1}{$v2}{$v3}{$v4}")
45+
""",
46+
yaml_content="""
47+
colang_version: "2.x"
48+
models:
49+
- type: main
50+
engine: openai
51+
model: gpt-3.5-turbo-instruct
52+
""",
53+
)
54+
55+
chat = TestChat(
56+
config,
57+
llm_completions=["'1'", "'2'", "'3'", "'4'"],
58+
)
59+
60+
chat >> "hi"
61+
chat << "1234"
62+
63+
64+
if __name__ == "__main__":
65+
test_1()

tests/v2_x/test_slide_mechanics.py

+32-5
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,12 @@ def test_expressions_in_strings():
229229

230230
content = """
231231
flow main
232-
start UtteranceBotAction(script="Roger") as $ref
232+
$xyz = 'x\\'y\\'z\\n'
233+
$test = '''Roger's
234+
test:\n {$xyz}'''
235+
start UtteranceBotAction(script=$test) as $ref
233236
start UtteranceBotAction(script="It's {{->}}")
234-
start UtteranceBotAction(script='It"s {{->}} \\'{$ref.start_event_arguments.script}!\\'')
237+
await UtteranceBotAction(script='It"s {{->}} \\'{$ref.start_event_arguments.script}!\\'')
235238
"""
236239

237240
config = _init_state(content)
@@ -241,20 +244,44 @@ def test_expressions_in_strings():
241244
[
242245
{
243246
"type": "StartUtteranceBotAction",
244-
"script": "Roger",
247+
"script": "Roger's\n test:\n x'y'z\n",
245248
},
246249
{
247250
"type": "StartUtteranceBotAction",
248251
"script": "It's {->}",
249252
},
250253
{
251254
"type": "StartUtteranceBotAction",
252-
"script": "It\"s {->} 'Roger!'",
255+
"script": "It\"s {->} 'Roger's\n test:\n x'y'z\n!'",
253256
},
254257
],
255258
)
256259

257260

261+
def test_multiline_string():
262+
"""Test string expression evaluation."""
263+
264+
content = '''
265+
flow main
266+
$var = "Test"
267+
$string = """This is a multiline
268+
string! '{$var}' "hi" """
269+
start UtteranceBotAction(script=$string)
270+
'''
271+
272+
config = _init_state(content)
273+
state = run_to_completion(config, start_main_flow_event)
274+
assert is_data_in_events(
275+
state.outgoing_events,
276+
[
277+
{
278+
"type": "StartUtteranceBotAction",
279+
"script": "This is a multiline\n string! 'Test' \"hi\" ",
280+
}
281+
],
282+
)
283+
284+
258285
def test_flow_return_values():
259286
"""Test flow return value handling."""
260287

@@ -950,4 +977,4 @@ def test_expression_evaluation():
950977

951978

952979
if __name__ == "__main__":
953-
test_expression_evaluation()
980+
test_multiline_string()

0 commit comments

Comments
 (0)