Skip to content
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

Fix tool calls parsing for definitions containing arrays #3389

Closed
1 change: 1 addition & 0 deletions src/promptflow-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
### Bugs fixed
- Fix ChatUI can't work in docker container when running image build with `pf flow build`.
- Fix [#3355](https://github.com/microsoft/promptflow/issues/3355) that IndexError is raised when generator is used in a flow and the flow is called inside another flow.
- Fix [#3388](https://github.com/microsoft/promptflow/issues/3388) that tool calls definitions with internal arrays failed parsing

## v1.11.0 (2024.05.17)

Expand Down
10 changes: 8 additions & 2 deletions src/promptflow-core/promptflow/core/_prompty_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,11 +653,17 @@ def try_parse_tool_call_id_and_content(role_prompt):
def try_parse_tool_calls(role_prompt):
# customer can add ## in front of tool_calls for markdown highlight.
# and we still support tool_calls without ## prefix for backward compatibility.
pattern = r"\n*#{0,2}\s*tool_calls\s*:\s*\n+\s*(\[.*?\])"

# Previously used pattern (commented) cannot parse tool_calls with internal square brackets
# common with function call arguments. Updated pattern matches content before tool_calls array
# and removes it before parsing the array.
# pattern = r"\n*#{0,2}\s*tool_calls\s*:\s*\n+\s*(\[.*?\])"
pattern = r"(\n*#{0,2}\s*tool_calls\s*:\s*\n+\s*)\["
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working in latest commit. I had to implement bracket counting, which I was hoping to avoid, to make this work since this test results in parsing of an assistant tool_calls message that has excess content due to an incorrect role definition. The previous implementation assumed the role_prompt would be parsed with ] as the last non-whitespace character which cannot be assumed based on this test case.

I also added additional tests that would fail under the previous implementation to help guide any future changes.

match = re.search(pattern, role_prompt, re.DOTALL)
if match:
try:
parsed_array = eval(match.group(1))
stripped_prompt = role_prompt.replace(match.group(1), "")
parsed_array = eval(stripped_prompt)
return parsed_array
except Exception:
None
Expand Down
3 changes: 3 additions & 0 deletions src/promptflow-tools/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
- Avoid unintended parsing by role process to user flow inputs in prompt templates.
- Introduce universal contract LLM tool and combine "Azure OpenAI GPT-4 Turbo with Vision" and "OpenAI GPT-4V" tools to "LLM-Vision" tool.

### Bugs Fixed
- Fix [#3388](https://github.com/microsoft/promptflow/issues/3388) that tool calls definitions with internal arrays failed parsing
bwilliams2 marked this conversation as resolved.
Show resolved Hide resolved

## 1.4.0 (2024.03.26)

### Features Added
Expand Down
28 changes: 26 additions & 2 deletions src/promptflow-tools/promptflow/tools/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,35 @@ def try_parse_tool_call_id_and_content(role_prompt):
def try_parse_tool_calls(role_prompt):
# customer can add ## in front of tool_calls for markdown highlight.
# and we still support tool_calls without ## prefix for backward compatibility.
pattern = r"\n*#{0,2}\s*tool_calls\s*:\s*\n+\s*(\[.*?\])"

# Previously used pattern (commented) cannot parse tool_calls with internal square brackets
# common with function call arguments. Updated pattern matches content before tool_calls array
# and removes it before parsing the array.
# pattern = r"\n*#{0,2}\s*tool_calls\s*:\s*\n+\s*(\[.*?\])"
# match = re.search(pattern, role_prompt, re.DOTALL)
pattern = r"(\s*\n*#{0,2}\s*tool_calls\s*:\s*\n+\s*)\["
match = re.search(pattern, role_prompt, re.DOTALL)
if match:
try:
parsed_array = eval(match.group(1))
stripped_prompt = role_prompt.replace(match.group(1), "")
# Find outer array brackets starting from [;
# Required for incomplete chunk parsing due to role naming errors
open_brackets = 1
last_open = 0
last_close = -1
# Loop until finding closed outer brackets or no more brackets found
while open_brackets != 0 and not (last_close == -1 and last_open == -1):
next_open = stripped_prompt.find("[", last_open + 1)
next_close = stripped_prompt.find("]", last_close + 1)
if next_open == -1 or next_open > next_close:
open_brackets -= 1
last_close = next_close
else:
open_brackets += 1
last_open = next_open
stripped_prompt = stripped_prompt[:last_close+1]

parsed_array = eval(stripped_prompt)
return parsed_array
except Exception:
None
Expand Down
16 changes: 16 additions & 0 deletions src/promptflow-tools/tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,22 @@ def test_try_parse_chat_with_tools(self, example_prompt_template_with_tool, pars
("tool_calls:\r\n[", None),
("tool_calls:\r\n[{'id': 'tool_call_id', 'type': 'function', 'function': {'name': 'func1', 'arguments': ''}}]",
[{'id': 'tool_call_id', 'type': 'function', 'function': {'name': 'func1', 'arguments': ''}}]),
("tool_calls:\r\n[{'id': 'tool_call_id', 'type': 'function',"
" 'function': {'name': 'func1', 'arguments': '{\"arg1\": []}'}}]",
[{'id': 'tool_call_id', 'type': 'function', 'function': {'name': 'func1', 'arguments': '{"arg1": []}'}}]),
("tool_calls:\r\n[{'id': 'tool_call_id', 'type': 'function',"
" 'function': {'name': 'func1', 'arguments': '{\"arg1\": [{\"nested\": []}]}'}}]",
[{'id': 'tool_call_id', 'type': 'function', 'function':
{'name': 'func1', 'arguments': '{"arg1": [{"nested": []}]}'}}]),
("tool_calls:\r\n[{'id': 'tool_call_id', 'type': 'function',"
" 'function': {'name': 'func1', 'arguments': '{\"arg1\": [{\"nested\": []}, {\"nested_2\": [[]]}]}'}}]",
[{'id': 'tool_call_id', 'type': 'function', 'function':
{'name': 'func1', 'arguments': '{"arg1": [{"nested": []}, {"nested_2": [[]]}]}'}}]),
("tool_calls:\r\n[{'id': 'tool_call_id', 'type': 'function',"
" 'function': {'name': 'func1', 'arguments': '{\"arg1\": [{\"nested\": []}, {\"nested_2\": [[]]}]}'}}]"
"\n #tool",
[{'id': 'tool_call_id', 'type': 'function', 'function':
{'name': 'func1', 'arguments': '{"arg1": [{"nested": []}, {"nested_2": [[]]}]}'}}]),
("tool_calls:\n[{'id': 'tool_call_id', 'type': 'function', 'function': {'name': 'func1', 'arguments': ''}}]",
[{'id': 'tool_call_id', 'type': 'function', 'function': {'name': 'func1', 'arguments': ''}}])])
def test_try_parse_tool_calls(self, role_prompt, expected_result):
Expand Down
Loading