diff --git a/tinker_cookbook/renderers.py b/tinker_cookbook/renderers.py index 5e6c876..2e87b6b 100644 --- a/tinker_cookbook/renderers.py +++ b/tinker_cookbook/renderers.py @@ -336,13 +336,20 @@ def _render_message(self, idx: int, message: Message) -> tuple[list[int], list[i # in the assistant messages, we so don't need to re-add it in those cases. ob_str += "\n" # Observation (prompt) part - ac_str = f"{ac_content}<|im_end|>" + if "tool_calls" in message: + ac_content += "\n".join( + [ + f"\n{json.dumps(tool_call)}\n" + for tool_call in message["tool_calls"] + ] + ) + ac_content += "<|im_end|>" # Action part ac_tail_str = "" # No action tail needed for Qwen format # Action part that's only included in the last message in SFT return ( self.tokenizer.encode(ob_str, add_special_tokens=False), - self.tokenizer.encode(ac_str, add_special_tokens=False), + self.tokenizer.encode(ac_content, add_special_tokens=False), self.tokenizer.encode(ac_tail_str, add_special_tokens=False), ) @@ -405,11 +412,10 @@ def parse_response(self, response: list[int]) -> tuple[Message, bool]: if not parse_success: return assistant_message, False - # NOTE: - # we use the ... tag to wrap the tool call. - match = re.search( - r"(.*?)", assistant_message["content"], re.DOTALL - ) + # Follow Qwen docs and Qwen-Agent's tool calling prompt to use ... tags to wrap the tool call. + # - https://qwen.readthedocs.io/en/latest/getting_started/concepts.html#tool-calling + # - https://github.com/QwenLM/Qwen-Agent/blob/main/qwen_agent/llm/fncall_prompts/nous_fncall_prompt.py#L279-L282 + match = re.search(r"(.*?)", assistant_message["content"], re.DOTALL) if match: tool_calls = self._parse_tool_call(match.group(1)) if tool_calls is None: