Repository navigation
fix(colang): parse generated actions from current OpenAI models #2516
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
base: pouyanpi/curly-quotes-minimal
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -438,10 +438,52 @@ def get_first_bot_intent(strings: List[str]) -> Optional[str]: | |
| return None | ||
|
|
||
|
|
||
| def _normalize_action_quotes(action: str) -> str: | ||
| """Normalize generated curly string delimiters, preserving quoted content.""" | ||
| if "“" not in action: | ||
| return action | ||
| result = [] | ||
| delimiter = None | ||
| curly_depth = 0 | ||
| escaped = False | ||
| for char in action: | ||
| if escaped: | ||
| result.append(char) | ||
| escaped = False | ||
| continue | ||
| if char == "\\" and delimiter is not None: | ||
| result.append(char) | ||
| escaped = True | ||
| continue | ||
| if delimiter == "“": | ||
| if char == "“": | ||
| curly_depth += 1 | ||
| elif char == "”": | ||
| curly_depth -= 1 | ||
| if curly_depth == 0: | ||
| delimiter = None | ||
| result.append('"') | ||
| continue | ||
| elif char == '"': | ||
| result.append('\\"') | ||
| continue | ||
| elif delimiter is not None: | ||
| if char == delimiter: | ||
| delimiter = None | ||
| elif char in ('"', "'", "“"): | ||
| delimiter = char | ||
|
Comment on lines
+470
to
+474
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Colang accepts triple-quoted action arguments. Track those delimiters too, and add a regression test that preserves their contents. Prompt To Fix With AIThis is a comment left during a code review.
Path: nemoguardrails/actions/llm/utils.py
Line: 470-474
Comment:
**Triple-quoted reply text changes**
`_normalize_action_quotes` changes content inside valid triple-quoted strings. For example, `bot say """She said "“hello”" today."""` becomes `bot say """She said ""hello"" today."""`. The interior straight quote closes the scanner’s string state, so it mistakes the curly quotes for delimiters and changes what the bot says.
Colang accepts triple-quoted action arguments. Track those delimiters too, and add a regression test that preserves their contents.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| if char == "“": | ||
| curly_depth = 1 | ||
| result.append('"') | ||
| continue | ||
| result.append(char) | ||
| return "".join(result) | ||
|
|
||
|
|
||
| def _has_unclosed_quote(s: str) -> bool: | ||
| """Check if a string has an unclosed double quote (ignoring escaped quotes).""" | ||
| # Strip escaped backslashes first so that \\" is not misread as \" (escaped quote). | ||
| return s.replace("\\\\", "").replace('\\"', "").count('"') % 2 == 1 | ||
| return _normalize_action_quotes(s).replace("\\\\", "").replace('\\"', "").count('"') % 2 == 1 | ||
|
|
||
|
|
||
| _MAX_QUOTE_CONTINUATION_LINES = 50 | ||
|
|
@@ -458,7 +500,7 @@ def get_first_bot_action(strings: List[str]) -> Optional[str]: | |
| if action and _has_unclosed_quote(action): | ||
| if continuation_lines >= _MAX_QUOTE_CONTINUATION_LINES: | ||
| # Safety bound: stop collecting to avoid absorbing all input. | ||
| return action | ||
| return _normalize_action_quotes(action) | ||
| action += "\\n" + string | ||
| continuation_lines += 1 | ||
| if not _has_unclosed_quote(action): | ||
|
|
@@ -467,16 +509,16 @@ def get_first_bot_action(strings: List[str]) -> Optional[str]: | |
| if string.startswith("bot action: "): | ||
| if action != "": | ||
| action += "\n" | ||
| action += string.replace("bot action: ", "") | ||
| action += string.removeprefix("bot action: ") | ||
| action_started = True | ||
| elif (string.startswith(" and") or string.startswith(" or")) and action_started: | ||
| action = action + string | ||
| elif string == "": | ||
| action_started = False | ||
| continue | ||
| elif action != "": | ||
| return action | ||
| return action | ||
| return _normalize_action_quotes(action) | ||
| return _normalize_action_quotes(action) | ||
|
|
||
|
|
||
| def escape_flow_name(name: str) -> str: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -389,7 +389,7 @@ async def generate_user_intent_and_bot_action( | |
| bot_intent = get_first_bot_intent(result.splitlines()) | ||
| bot_action = get_first_bot_action(result.splitlines()) | ||
|
|
||
| if bot_action is None: | ||
| if not bot_action or not bot_action.strip(): | ||
| raise LlmResponseError(f"Issue with LLM response: {result}") | ||
|
|
||
| user_intent = escape_flow_name(user_intent.strip(" ")) | ||
|
|
@@ -705,7 +705,7 @@ async def generate_flow_continuation( | |
| bot_intent = get_first_bot_intent(result.splitlines()) | ||
| bot_action = get_first_bot_action(result.splitlines()) | ||
|
|
||
| if bot_action is None: | ||
| if not bot_action or not bot_action.strip(): | ||
| raise LlmResponseError(f"Issue with LLM response: {result}") | ||
|
|
||
| if bot_intent: | ||
|
|
@@ -719,7 +719,7 @@ async def generate_flow_continuation( | |
| return { | ||
| "name": flow_name, | ||
| "parameters": flow_parameters, | ||
| "body": f'@meta(bot_intent="{bot_intent}")\n' + f"flow {flow_name}\n" + f" {bot_action}", | ||
| "body": f'@meta(bot_intent="{bot_intent}")\n' + f"flow {flow_name}\n" + textwrap.indent(bot_action, " "), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| @action(name="CreateFlowAction", is_system_action=True, execute_async=True) | ||
|
|
@@ -738,7 +738,7 @@ async def create_flow( | |
| flow_name = f"_dynamic_{uuid} {name}" | ||
| # TODO: parse potential parameters from flow name with a regex | ||
|
|
||
| body = f"flow {flow_name}\n " + body | ||
| body = f"flow {flow_name}\n" + textwrap.indent(body, " ") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve already-indented If
|
||
| if decorators: | ||
| body = decorators + "\n" + body | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| prompts: | ||
| - task: generate_user_intent | ||
| models: &models | ||
| - openai/gpt-5 | ||
| - openai/gpt-6 | ||
| messages: | ||
| - type: system | ||
| content: |- | ||
| {{ general_instructions }} | ||
| Classify the latest user message into a Colang 1 user intent. | ||
| Prefer a matching intent from the examples. If none matches, generate | ||
| a short intent name that describes the user's request. | ||
| Return exactly one line: User intent: <intent name> | ||
| Do not answer the user, repeat their message, or include explanations | ||
| or Markdown fences. | ||
| - type: user | ||
| content: |- | ||
| Example conversation: | ||
| {{ sample_conversation | verbose_v1 }} | ||
| Intent examples: | ||
| {{ examples | verbose_v1 }} | ||
| Candidate intents: {{ potential_user_intents }} | ||
| Conversation: | ||
| {{ history | colang | verbose_v1 }} | ||
| output_parser: user_intent | ||
|
|
||
| - task: generate_next_steps | ||
| models: *models | ||
| messages: | ||
| - type: system | ||
| content: |- | ||
| {{ general_instructions }} | ||
| Generate the next Colang 1 flow steps for the latest user intent, | ||
| following the matching examples. For a bot response, return only: | ||
| Bot intent: <intent name> | ||
| When an example calls for actions or other flow steps, return those | ||
| Colang steps instead. Do not write the spoken answer, repeat the user | ||
| intent, or include explanations or Markdown fences. | ||
| - type: user | ||
| content: |- | ||
| Example conversation: | ||
| {{ sample_conversation | remove_text_messages | verbose_v1 }} | ||
| Flow examples: | ||
| {{ examples | remove_text_messages | verbose_v1 }} | ||
| Conversation: | ||
| {{ history | colang | remove_text_messages | verbose_v1 }} | ||
| output_parser: verbose_v1 | ||
|
|
||
| - task: generate_bot_message | ||
| models: *models | ||
| messages: | ||
| - type: system | ||
| content: |- | ||
| {{ general_instructions }} | ||
| Write the spoken response for the latest bot intent in the conversation. | ||
| Return only: Bot message: "<response>" | ||
| Use straight double quotes around the response. Do not repeat intents, | ||
| invent another turn, or include explanations or Markdown fences. | ||
| - type: user | ||
| content: |- | ||
| Example conversation: | ||
| {{ sample_conversation | verbose_v1 }} | ||
| Bot response examples: | ||
| {{ examples | verbose_v1 }} | ||
| {% if relevant_chunks %} | ||
| Reference information: | ||
| {{ relevant_chunks }} | ||
| {% endif %} | ||
| Conversation: | ||
| {{ history | colang | verbose_v1 }} | ||
| output_parser: bot_message | ||
|
|
||
| - task: generate_intent_steps_message | ||
| models: *models | ||
| messages: | ||
| - type: system | ||
| content: |- | ||
| {{ general_instructions }} | ||
| Generate the next turn of a Colang 1 interaction, using exactly this format: | ||
| User intent: <intent name> | ||
| Bot intent: <intent name> | ||
| Bot message: "<response>" | ||
| Prefer a matching user intent from the examples. If none matches, | ||
| generate a short intent name that describes the user's request. | ||
| Use straight double quotes around the response. Generate only these | ||
| two intents and the bot message. Do not repeat the user message, | ||
| invent another turn, or include explanations or Markdown fences. | ||
| - type: user | ||
| content: |- | ||
| Example conversation: | ||
| {{ sample_conversation | verbose_v1 }} | ||
| Flow examples: | ||
| {{ examples | verbose_v1 }} | ||
| Candidate intents: {{ potential_user_intents }} | ||
| {% if relevant_chunks %} | ||
| Reference information: | ||
| {{ relevant_chunks }} | ||
| {% endif %} | ||
| Conversation: | ||
| {{ history | colang | verbose_v1 }} | ||
| output_parser: verbose_v1 | ||
|
|
||
| - task: generate_value | ||
| models: *models | ||
| messages: | ||
| - type: system | ||
| content: |- | ||
| {{ general_instructions }} | ||
| Generate a value for the variable {{ var_name }} following the instruction. | ||
| Return only a Python literal on one line, with no assignment, explanation, | ||
| or Markdown fences. Use True and False for booleans and straight quotes | ||
| for strings. | ||
| - type: user | ||
| content: |- | ||
| Flow examples: | ||
| {{ examples }} | ||
| Conversation: | ||
| {{ history | colang }} | ||
| Instruction: {{ instructions }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Limit the curly-quote claim to generated actions.
get_first_bot_actionconverts curly delimiters before parsing. The Colang grammar does not accept curly delimiters in a.costring. A reader who writesbot say “Hi”in a flow will get a parse error. State that generated LLM actions are normalized; require straight quotes in Colang source. (raw.githubusercontent.com)