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 <noreply@anthropic.com>
This commit is contained in:
Suppai_NekoandClaude Sonnet 4.6 committed 2026-04-01 11:40:01 -07:00
1 parent d53835bb5f
commit 65982bb636
1 file changed
+32 -8
+32 -8
View File
@@ -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})