diff --git a/app/response_parser.py b/app/response_parser.py index 2ce5508..a7fbe96 100644 --- a/app/response_parser.py +++ b/app/response_parser.py @@ -125,6 +125,7 @@ class ResponseParser: 1. Responses with tool calls (wrapped in tags) 2. Regular text responses 3. Multiple tool calls in a single response + 4. Incomplete tool calls (missing closing tag) - fallback parsing Args: llm_response: The raw text response from the LLM @@ -152,6 +153,10 @@ class ResponseParser: if matches: return self._parse_tool_call_response(llm_response, matches) else: + # Check for incomplete tool call (opening tag without closing tag) + if self.tool_call_start_tag in llm_response: + logger.warning("Detected incomplete tool call (missing closing tag). Attempting fallback parsing.") + return self._parse_incomplete_tool_call(llm_response) return self._parse_text_only_response(llm_response) except Exception as e: @@ -217,6 +222,55 @@ class ResponseParser: tool_calls=tool_calls if tool_calls else None ) + def _parse_incomplete_tool_call(self, llm_response: str) -> ResponseMessage: + """ + Parse a response with an incomplete tool call (missing closing tag). + + This is a fallback method when the LLM doesn't close the tag properly. + It attempts to extract the tool call JSON and complete it. + + Args: + llm_response: The full LLM response with incomplete tool call + + Returns: + ResponseMessage with content and optionally tool_calls + """ + try: + # Find the opening tag + start_idx = llm_response.find(self.tool_call_start_tag) + if start_idx == -1: + return self._parse_text_only_response(llm_response) + + # Extract content before the opening tag + content_before = llm_response[:start_idx].strip() if start_idx > 0 else None + + # Extract everything after the opening tag + after_tag = llm_response[start_idx + len(self.tool_call_start_tag):] + + # Try to extract valid JSON + json_str = self._extract_valid_json(after_tag) + if json_str: + try: + tool_call_data = json.loads(json_str) + tool_call = self._create_tool_call(tool_call_data) + + logger.info(f"Successfully parsed incomplete tool call: {tool_call.function.name}") + + return ResponseMessage( + content=content_before, + tool_calls=[tool_call] + ) + except json.JSONDecodeError as e: + logger.warning(f"Failed to parse JSON from incomplete tool call: {e}") + + # If all else fails, return as text + logger.warning("Could not salvage incomplete tool call, returning as text") + return ResponseMessage(content=llm_response) + + except Exception as e: + logger.warning(f"Error in _parse_incomplete_tool_call: {e}") + return ResponseMessage(content=llm_response) + def _parse_text_only_response(self, llm_response: str) -> ResponseMessage: """ Parse a response with no tool calls. diff --git a/app/services.py b/app/services.py index 92e9594..0735ce1 100644 --- a/app/services.py +++ b/app/services.py @@ -117,17 +117,33 @@ def inject_tools_into_prompt(messages: List[ChatMessage], tools: List[Tool]) -> tool_prompt = f""" You are a helpful assistant with access to a set of tools. -You can call them by emitting a JSON object inside tool call tags. -IMPORTANT: Use the following format for tool calls: -Format: {TOOL_CALL_START_TAG}{{"name": "tool_name", "arguments": {{...}}}}{TOOL_CALL_END_TAG} +## TOOL CALL FORMAT (CRITICAL) -Example: {full_example} +When you need to use a tool, you MUST follow this EXACT format: -Here are the available tools: +{TOOL_CALL_START_TAG}{{"name": "tool_name", "arguments": {{...}}}}{TOOL_CALL_END_TAG} + +### IMPORTANT RULES: +1. ALWAYS include BOTH the opening tag ({TOOL_CALL_START_TAG}) AND closing tag ({TOOL_CALL_END_TAG}) +2. The JSON must be valid and properly formatted +3. Keep arguments concise to avoid truncation +4. Do not include any text between the tags except the JSON + +### Examples: +Simple call: +{full_example} + +Multiple arguments: +{TOOL_CALL_START_TAG}{{"name": "search", "arguments": {{"query": "example", "limit": 5}}}}{TOOL_CALL_END_TAG} + +## AVAILABLE TOOLS: {tool_defs} -Only use the tools if strictly necessary. +## REMEMBER: +- If you decide to call a tool, output ONLY the tool call tags (you may add brief text before or after) +- ALWAYS close your tags properly with {TOOL_CALL_END_TAG} +- Keep your arguments concise and essential """ # Prepend the system prompt with tool definitions return [ChatMessage(role="system", content=tool_prompt)] + messages