From e8e94f2ebef521f4561b493c99f2c157ccdb498b Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Thu, 6 Nov 2025 14:59:12 -0800 Subject: [PATCH] fix(core): patch bugs on transcript formatting for sleeptime w/ HITL (#6025) --- letta/groups/helpers.py | 57 +++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/letta/groups/helpers.py b/letta/groups/helpers.py index 3b7f70d0..ea1503d7 100644 --- a/letta/groups/helpers.py +++ b/letta/groups/helpers.py @@ -87,6 +87,7 @@ def load_multi_agent( def stringify_message(message: Message, use_assistant_name: bool = False) -> str | None: assistant_name = message.name or "assistant" if use_assistant_name else "assistant" + if message.role == "user": try: messages = [] @@ -97,7 +98,9 @@ def stringify_message(message: Message, use_assistant_name: bool = False) -> str messages.append(f"{message.name or 'user'}: [Image Here]") return "\n".join(messages) except: - return f"{message.name or 'user'}: {message.content[0].text}" + if message.content and len(message.content) > 0: + return f"{message.name or 'user'}: {message.content[0].text}" + return None elif message.role == "assistant": messages = [] if message.content: @@ -105,13 +108,42 @@ def stringify_message(message: Message, use_assistant_name: bool = False) -> str if isinstance(content, TextContent): messages.append(f"{assistant_name}: {content.text}") elif isinstance(content, ReasoningContent): - messages.append(f"{assistant_name}: *thinking* {content.reasoning}") + messages.append(f"{assistant_name}: {content.reasoning}") if message.tool_calls: - if message.tool_calls[0].function.name == "send_message": - messages.append(f"{assistant_name}: {json.loads(message.tool_calls[0].function.arguments)['message']}") - else: - messages.append(f"{assistant_name}: Calling tool {message.tool_calls[0].function.name}") - return "\n".join(messages) + for tool_call in message.tool_calls: + if tool_call.function.name == "send_message": + messages.append(f"{assistant_name}: {json.loads(tool_call.function.arguments)['message']}") + else: + messages.append(f"{assistant_name}: Calling tool {tool_call.function.name}") + return "\n".join(messages) if messages else None + elif message.role == "approval": + # role == "approval" has two cases: + # 1. Approval REQUEST: has tool_calls (assistant calling tool that needs HITL) + # 2. Approval RESPONSE: no tool_calls, has approve field (user's decision) + + # Check if this is an approval request (has tool_calls) + if hasattr(message, "tool_calls") and message.tool_calls: + # Treat like assistant message calling a tool + messages = [] + if message.content: + for content in message.content: + if isinstance(content, TextContent): + messages.append(f"{assistant_name}: {content.text}") + elif isinstance(content, ReasoningContent): + messages.append(f"{assistant_name}: {content.reasoning}") + for tool_call in message.tool_calls: + if tool_call.function.name == "send_message": + messages.append(f"{assistant_name}: {json.loads(tool_call.function.arguments)['message']}") + else: + messages.append(f"{assistant_name}: Calling tool {tool_call.function.name}") + return "\n".join(messages) if messages else None + else: + # Approval response - user approved/rejected + if hasattr(message, "approve") and message.approve is not None: + status = "approved" if message.approve else "rejected" + reason = f": {message.denial_reason}" if hasattr(message, "denial_reason") and message.denial_reason else "" + return f"[User {status}{reason}]" + return None elif message.role == "tool": if message.content: content = json.loads(message.content[0].text) @@ -121,4 +153,13 @@ def stringify_message(message: Message, use_assistant_name: bool = False) -> str elif message.role == "system": return None else: - return f"{message.name or 'user'}: {message.content[0].text}" + if message.content and len(message.content) > 0: + # Handle different content types + content_item = message.content[0] + if isinstance(content_item, TextContent): + return f"{message.name or 'user'}: {content_item.text}" + elif isinstance(content_item, ReasoningContent): + return f"{message.name or 'user'}: {content_item.reasoning}" + elif isinstance(content_item, ImageContent): + return f"{message.name or 'user'}: [Image Here]" + return None