Skip to content

Commit bb7e7c4

Browse files
authored
Merge pull request #594
Add more information to Colang syntax errors
2 parents 76e6343 + cfae133 commit bb7e7c4

File tree

11 files changed

+166
-105
lines changed

11 files changed

+166
-105
lines changed

nemoguardrails/actions/action_dispatcher.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,17 @@ async def execute_action(
236236
raise e
237237

238238
except Exception as e:
239-
log.warning(f"Error while execution {action_name}: {e}")
239+
filtered_params = {
240+
k: v
241+
for k, v in params.items()
242+
if k not in ["state", "events", "llm"]
243+
}
244+
log.warning(
245+
"Error while execution '%s' with parameters '%s': %s",
246+
action_name,
247+
filtered_params,
248+
e,
249+
)
240250
log.exception(e)
241251

242252
return None, "failed"

nemoguardrails/actions/v2_x/generation.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,10 @@ async def generate_value(
653653

654654
log.info("Generated value for $%s: %s", var_name, value)
655655

656-
return literal_eval(value)
656+
try:
657+
return literal_eval(value)
658+
except Exception:
659+
raise Exception(f"Invalid LLM response: `{value}`")
657660

658661
@action(name="GenerateFlowAction", is_system_action=True, execute_async=True)
659662
async def generate_flow(

nemoguardrails/colang/v2_x/lang/expansion.py

+65-45
Original file line numberDiff line numberDiff line change
@@ -55,49 +55,69 @@ def expand_elements(
5555
elements_changed = False
5656
new_elements: List[ElementType] = []
5757
for element in elements:
58-
expanded_elements: List[ElementType] = []
59-
if isinstance(element, SpecOp):
60-
if element.op == "send":
61-
expanded_elements = _expand_send_element(element)
62-
elif element.op == "match":
63-
expanded_elements = _expand_match_element(element)
64-
elif element.op == "start":
65-
expanded_elements = _expand_start_element(element)
66-
elif element.op == "stop":
67-
expanded_elements = _expand_stop_element(element)
68-
elif element.op == "activate":
69-
expanded_elements = _expand_activate_element(element)
70-
elif element.op == "await":
71-
expanded_elements = _expand_await_element(element)
72-
elif isinstance(element, Assignment):
73-
expanded_elements = _expand_assignment_stmt_element(element)
74-
elif isinstance(element, While):
75-
expanded_elements = _expand_while_stmt_element(element, flow_configs)
76-
elif isinstance(element, If):
77-
expanded_elements = _expand_if_element(element, flow_configs)
78-
elements_changed = True # Makes sure to update continue/break elements
79-
elif isinstance(element, When):
80-
expanded_elements = _expand_when_stmt_element(element, flow_configs)
81-
elements_changed = True # Makes sure to update continue/break elements
82-
elif isinstance(element, Continue):
83-
if element.label is None and continue_break_labels is not None:
84-
element.label = continue_break_labels[0]
85-
elif isinstance(element, Break):
86-
if element.label is None and continue_break_labels is not None:
87-
element.label = continue_break_labels[1]
88-
89-
if len(expanded_elements) > 0:
90-
# Map new elements to source
91-
for expanded_element in expanded_elements:
92-
if isinstance(expanded_element, Element) and isinstance(
93-
element, Element
94-
):
95-
expanded_element._source = element._source
96-
# Add new elements
97-
new_elements.extend(expanded_elements)
98-
elements_changed = True
99-
else:
100-
new_elements.extend([element])
58+
try:
59+
expanded_elements: List[ElementType] = []
60+
if isinstance(element, SpecOp):
61+
if element.op == "send":
62+
expanded_elements = _expand_send_element(element)
63+
elif element.op == "match":
64+
expanded_elements = _expand_match_element(element)
65+
elif element.op == "start":
66+
expanded_elements = _expand_start_element(element)
67+
elif element.op == "stop":
68+
expanded_elements = _expand_stop_element(element)
69+
elif element.op == "activate":
70+
expanded_elements = _expand_activate_element(element)
71+
elif element.op == "await":
72+
expanded_elements = _expand_await_element(element)
73+
elif isinstance(element, Assignment):
74+
expanded_elements = _expand_assignment_stmt_element(element)
75+
elif isinstance(element, While):
76+
expanded_elements = _expand_while_stmt_element(
77+
element, flow_configs
78+
)
79+
elif isinstance(element, If):
80+
expanded_elements = _expand_if_element(element, flow_configs)
81+
elements_changed = (
82+
True # Makes sure to update continue/break elements
83+
)
84+
elif isinstance(element, When):
85+
expanded_elements = _expand_when_stmt_element(element, flow_configs)
86+
elements_changed = (
87+
True # Makes sure to update continue/break elements
88+
)
89+
elif isinstance(element, Continue):
90+
if element.label is None and continue_break_labels is not None:
91+
element.label = continue_break_labels[0]
92+
elif isinstance(element, Break):
93+
if element.label is None and continue_break_labels is not None:
94+
element.label = continue_break_labels[1]
95+
96+
if len(expanded_elements) > 0:
97+
# Map new elements to source
98+
for expanded_element in expanded_elements:
99+
if isinstance(expanded_element, Element) and isinstance(
100+
element, Element
101+
):
102+
expanded_element._source = element._source
103+
# Add new elements
104+
new_elements.extend(expanded_elements)
105+
elements_changed = True
106+
else:
107+
new_elements.extend([element])
108+
109+
except Exception as e:
110+
error = "Error"
111+
if e.args[0]:
112+
error = e.args[0]
113+
114+
if hasattr(element, "_source") and element._source:
115+
# TODO: Resolve source line to Colang file level
116+
raise ColangSyntaxError(
117+
error + f" on source line {element._source.line}"
118+
)
119+
else:
120+
raise ColangSyntaxError(error)
101121

102122
elements = new_elements
103123
return elements
@@ -476,7 +496,7 @@ def _expand_await_element(
476496
)
477497
else:
478498
raise ColangSyntaxError(
479-
f"Unsupported spec type '{type(element.spec)}', element '{element.spec.name}' on line {element._source.line}"
499+
f"Unsupported spec type '{type(element.spec)}', element '{element.spec.name}'"
480500
)
481501
else:
482502
# Element group
@@ -631,7 +651,7 @@ def _expand_activate_element(
631651
else:
632652
# It's an UMIM event
633653
raise ColangSyntaxError(
634-
f"Only flows can be activated but not '{element.spec.spec_type}', element '{element.spec.name}' on line {element._source.line}!"
654+
f"Only flows can be activated but not '{element.spec.spec_type}', element '{element.spec.name}'"
635655
)
636656
elif isinstance(element.spec, dict):
637657
# Multiple match elements

nemoguardrails/colang/v2_x/lang/parser.py

+8-15
Original file line numberDiff line numberDiff line change
@@ -137,27 +137,20 @@ def _contains_exclude_from_llm_tag(self, content: str) -> bool:
137137

138138

139139
def parse_colang_file(
140-
_filename: str, content: str, include_source_mapping: bool = True
140+
filename: str, content: str, include_source_mapping: bool = True
141141
) -> dict:
142142
"""Parse the content of a .co."""
143143

144144
colang_parser = ColangParser(include_source_mapping=include_source_mapping)
145145
result = colang_parser.parse_content(content, print_tokens=False)
146146

147-
# flows = []
148-
# for flow_data in result["flows"]:
149-
# # elements = parse_flow_elements(items)
150-
# # TODO: extract the source code here
151-
# source_code = ""
152-
# flows.append(
153-
# {
154-
# "id": flow_data["name"],
155-
# "elements": flow_data["elements"],
156-
# "source_code": source_code,
157-
# }
158-
# )
159-
160-
data = {"flows": result["flows"], "import_paths": result.get("import_paths", [])}
147+
for flow in result["flows"]:
148+
flow["file_info"]["name"] = filename
149+
150+
data = {
151+
"flows": result["flows"],
152+
"import_paths": result.get("import_paths", []),
153+
}
161154

162155
return data
163156

nemoguardrails/colang/v2_x/lang/utils.py

+7
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,10 @@ def dataclass_to_dict(obj: Any) -> Any:
2626
return {k: dataclass_to_dict(v) for k, v in obj.items()}
2727
else:
2828
return obj
29+
30+
31+
def format_colang_parsing_error_message(exception, colang_content):
32+
"""Improves readability of Colang error messages."""
33+
line = colang_content.splitlines()[exception.line - 1]
34+
marker = " " * (exception.column - 1) + "^"
35+
return f"{exception}:\n{line}\n{marker}"

nemoguardrails/colang/v2_x/runtime/eval.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def eval_expression(expr: str, context: dict) -> Any:
183183

184184
return result
185185
except Exception as e:
186-
raise ColangValueError(f"Error evaluating '{expr}'") from e
186+
raise ColangValueError(f"Error evaluating '{expr}', {e}")
187187

188188

189189
def _create_regex(pattern: str) -> re.Pattern:

nemoguardrails/colang/v2_x/runtime/flows.py

+3
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,9 @@ class FlowConfig:
372372
# The actual source code, if available
373373
source_code: Optional[str] = None
374374

375+
# The name of the source code file
376+
source_file: Optional[str] = None
377+
375378
@property
376379
def loop_id(self) -> Optional[str]:
377380
"""Return the interaction loop id if set."""

nemoguardrails/colang/v2_x/runtime/runtime.py

+36-28
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from nemoguardrails.colang import parse_colang_file
2828
from nemoguardrails.colang.runtime import Runtime
2929
from nemoguardrails.colang.v2_x.lang.colang_ast import Decorator, Flow
30+
from nemoguardrails.colang.v2_x.lang.utils import format_colang_parsing_error_message
3031
from nemoguardrails.colang.v2_x.runtime.errors import (
3132
ColangRuntimeError,
3233
ColangSyntaxError,
@@ -82,38 +83,44 @@ async def _add_flows_action(self, state: "State", **args: dict) -> List[str]:
8283
version="2.x",
8384
include_source_mapping=True,
8485
)
85-
except Exception as e:
86-
log.warning("Failed parsing a generated flow\n%s\n%s", flow_content, e)
87-
return []
88-
# Alternatively, we could through an exceptions
89-
# raise ColangRuntimeError(f"Could not parse the generated Colang code! {ex}")
90-
91-
added_flows: List[str] = []
92-
for flow in parsed_flow["flows"]:
93-
if flow.name in state.flow_configs:
94-
log.warning("Flow '%s' already exists! Not loaded!", flow.name)
95-
break
96-
97-
flow_config = FlowConfig(
98-
id=flow.name,
99-
elements=expand_elements(flow.elements, state.flow_configs),
100-
decorators=convert_decorator_list_to_dictionary(flow.decorators),
101-
parameters=flow.parameters,
102-
return_members=flow.return_members,
103-
source_code=flow.source_code,
104-
)
10586

106-
# Print out expanded flow elements
107-
# json.dump(flow_config, sys.stdout, indent=4, cls=EnhancedJsonEncoder)
87+
added_flows: List[str] = []
88+
for flow in parsed_flow["flows"]:
89+
if flow.name in state.flow_configs:
90+
log.warning("Flow '%s' already exists! Not loaded!", flow.name)
91+
break
92+
93+
flow_config = FlowConfig(
94+
id=flow.name,
95+
elements=expand_elements(flow.elements, state.flow_configs),
96+
decorators=convert_decorator_list_to_dictionary(flow.decorators),
97+
parameters=flow.parameters,
98+
return_members=flow.return_members,
99+
source_code=flow.source_code,
100+
)
101+
102+
# Alternatively, we could through an exceptions
103+
# raise ColangRuntimeError(f"Could not parse the generated Colang code! {ex}")
108104

109-
initialize_flow(state, flow_config)
105+
# Print out expanded flow elements
106+
# json.dump(flow_config, sys.stdout, indent=4, cls=EnhancedJsonEncoder)
110107

111-
# Add flow config to state.flow_configs
112-
state.flow_configs.update({flow.name: flow_config})
108+
initialize_flow(state, flow_config)
113109

114-
added_flows.append(flow.name)
110+
# Add flow config to state.flow_configs
111+
state.flow_configs.update({flow.name: flow_config})
115112

116-
return added_flows
113+
added_flows.append(flow.name)
114+
115+
return added_flows
116+
117+
except Exception as e:
118+
log.warning(
119+
"Failed parsing a generated flow\n%s\n%s",
120+
flow_content,
121+
format_colang_parsing_error_message(e, flow_content),
122+
)
123+
return []
117124

118125
async def _remove_flows_action(self, state: "State", **args: dict) -> None:
119126
log.info("Start RemoveFlowsAction! %s", args)
@@ -499,7 +506,7 @@ async def process_events(
499506
run_to_completion(state, new_event)
500507
new_event = None
501508
except Exception as e:
502-
log.warning("Colang error!", exc_info=True)
509+
log.warning("Colang runtime error!", exc_info=True)
503510
new_event = Event(
504511
name="ColangError",
505512
arguments={
@@ -708,6 +715,7 @@ def create_flow_configs_from_flow_list(flows: List[Flow]) -> Dict[str, FlowConfi
708715
parameters=flow.parameters,
709716
return_members=flow.return_members,
710717
source_code=flow.source_code,
718+
source_file=flow.file_info["name"],
711719
)
712720

713721
if config.is_override:

nemoguardrails/colang/v2_x/runtime/statemachine.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from nemoguardrails.colang.v2_x.lang.expansion import expand_elements
5050
from nemoguardrails.colang.v2_x.runtime.errors import (
5151
ColangRuntimeError,
52+
ColangSyntaxError,
5253
ColangValueError,
5354
)
5455
from nemoguardrails.colang.v2_x.runtime.eval import (
@@ -87,9 +88,15 @@ def initialize_state(state: State) -> None:
8788

8889
state.flow_states = dict()
8990

90-
# TODO: Think about where to put this
91-
for flow_config in state.flow_configs.values():
92-
initialize_flow(state, flow_config)
91+
try:
92+
# TODO: Think about where to put this
93+
for flow_config in state.flow_configs.values():
94+
initialize_flow(state, flow_config)
95+
except Exception as e:
96+
if e.args[0]:
97+
raise ColangSyntaxError(e.args[0] + f" (flow `{flow_config.id}`)")
98+
else:
99+
raise ColangSyntaxError() from e
93100

94101
# Create main flow state first
95102
main_flow_config = state.flow_configs["main"]
@@ -920,9 +927,16 @@ def _advance_head_front(state: State, heads: List[FlowHead]) -> List[FlowHead]:
920927
actionable_heads.append(head)
921928
except Exception as e:
922929
# In case there were any runtime error the flow will be aborted (fail)
930+
source_line = "unknown"
931+
element = flow_config.elements[head.position]
932+
if hasattr(element, "_source") and element._source:
933+
source_line = str(element._source.line)
923934
log.warning(
924-
"Colang error: Flow '%s' failed due to runtime exception!",
935+
"Flow '%s' failed on line %s (%s) due to Colang runtime exception: %s",
925936
flow_state.flow_id,
937+
source_line,
938+
flow_config.source_file,
939+
e,
926940
exc_info=True,
927941
)
928942
colang_error_event = Event(

nemoguardrails/rails/llm/config.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from nemoguardrails.colang import parse_colang_file, parse_flow_elements
2626
from nemoguardrails.colang.v2_x.lang.colang_ast import Flow
27+
from nemoguardrails.colang.v2_x.lang.utils import format_colang_parsing_error_message
2728
from nemoguardrails.colang.v2_x.runtime.errors import ColangParsingError
2829

2930
log = logging.getLogger(__name__)
@@ -636,12 +637,14 @@ def _parse_colang_files_recursively(
636637

637638
with open(current_path, "r", encoding="utf-8") as f:
638639
try:
640+
content = f.read()
639641
_parsed_config = parse_colang_file(
640-
current_file, content=f.read(), version=colang_version
642+
current_file, content=content, version=colang_version
641643
)
642644
except Exception as e:
643645
raise ColangParsingError(
644-
f"Error while parsing Colang file: {current_path}"
646+
f"Error while parsing Colang file: {current_path}\n"
647+
+ format_colang_parsing_error_message(e, content)
645648
) from e
646649

647650
# We join only the "import_paths" field in the config for now

0 commit comments

Comments
 (0)