From 65982bb636e603942d79d407662bf41ddc6a0b17 Mon Sep 17 00:00:00 2001 From: Cole Date: Wed, 1 Apr 2026 11:40:01 -0700 Subject: [PATCH] Fix Qwen 400 template errors and add retry logic - Add _assistant_msg_dict() to serialize assistant messages as plain dicts with content normalized from None to "", preventing Qwen's jinja template from rejecting tool-call messages with content=null - Apply fix in all three agent loops (_run_agent_inner, run_reflection_agent, run_life_event_reflection) - Replace error-to-chat fallback with silent logging + up to 3 retries (2s apart) before giving up Co-Authored-By: Claude Sonnet 4.6 --- bot.py | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/bot.py b/bot.py index b276911..a9a2efc 100644 --- a/bot.py +++ b/bot.py @@ -678,6 +678,24 @@ async def send_image_to_channel(url: str, channel: discord.abc.Messageable, capt return f"Failed to fetch or send image: {exc}" +def _assistant_msg_dict(msg) -> dict: + """Serialize an assistant ChatCompletionMessage to a plain dict. + + Qwen's jinja template rejects content=null, so we normalize it to "". + """ + d: dict = {"role": "assistant", "content": msg.content or ""} + if msg.tool_calls: + d["tool_calls"] = [ + { + "id": tc.id, + "type": "function", + "function": {"name": tc.function.name, "arguments": tc.function.arguments}, + } + for tc in msg.tool_calls + ] + return d + + def _ensure_user_tail(messages: list) -> None: """Qwen's chat template requires a non-empty user message as the last message.""" if not messages: @@ -750,7 +768,7 @@ async def _run_agent_inner(messages: list, channel: discord.abc.Messageable | No if not msg.tool_calls: return msg.content - kwargs["messages"].append(msg) + kwargs["messages"].append(_assistant_msg_dict(msg)) for tc in msg.tool_calls: args = json.loads(tc.function.arguments) @@ -822,7 +840,7 @@ async def run_reflection_agent(messages: list, max_turns: int) -> tuple[str, str if not msg.tool_calls: break - kwargs["messages"].append(msg) + kwargs["messages"].append(_assistant_msg_dict(msg)) for tc in msg.tool_calls: args = json.loads(tc.function.arguments) @@ -875,7 +893,7 @@ async def run_life_event_reflection(messages: list): if not msg.tool_calls: break - kwargs["messages"].append(msg) + kwargs["messages"].append(_assistant_msg_dict(msg)) for tc in msg.tool_calls: args = json.loads(tc.function.arguments) @@ -1244,11 +1262,17 @@ async def on_message(message: discord.Message): messages = [{"role": "system", "content": system_content}] + list(history)[:-1] + [current_msg] async with message.channel.typing(): - try: - reply = await run_agent(messages, channel=message.channel) - except Exception as exc: - reply = f"Error: {exc}" - history.pop() + reply = None + for attempt in range(3): + try: + reply = await run_agent(messages, channel=message.channel) + break + except Exception as exc: + log.error(f"[agent] Error (attempt {attempt + 1}/3): {exc}") + if attempt == 2: + history.pop() + return + await asyncio.sleep(2) if reply: history.append({"role": "assistant", "content": reply})