7474055c97
- Native reply threading: inbound replies continue their conversation via SendBlue message_handle lookup; standalone messages create a new conversation - Our responses are sent with reply_to so they thread visually in iMessage - Typing indicator fires before agent invocation and stops in a finally block - Add extract_agent_text() to handle list-type AIMessage content and strip <think> tokens from Qwen3 — applied to iMessage, WhatsApp, and web chat - ConversationMessage gains a nullable message_handle field (migration needed) - /threads command lists recent iMessage conversations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
57 lines
2.1 KiB
Python
57 lines
2.1 KiB
Python
import re
|
|
|
|
_THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL)
|
|
|
|
|
|
def extract_agent_text(content) -> str:
|
|
"""Extract plain text from a LangChain AIMessage content.
|
|
|
|
Handles both str and list-of-blocks content (the latter occurs with some
|
|
tool-using or vision models), and strips <think>...</think> tokens emitted
|
|
by reasoning models like Qwen3.
|
|
"""
|
|
if isinstance(content, list):
|
|
text = "".join(
|
|
block.get("text", "")
|
|
if isinstance(block, dict) and block.get("type") != "thinking"
|
|
else (str(block) if not isinstance(block, dict) else "")
|
|
for block in content
|
|
)
|
|
else:
|
|
text = str(content) if content is not None else ""
|
|
return _THINK_RE.sub("", text).strip()
|
|
|
|
|
|
def strip_markdown(text: str) -> str:
|
|
"""Strip markdown formatting from text for plain-text channels like iMessage."""
|
|
# Code blocks (fenced)
|
|
text = re.sub(
|
|
r"```[\s\S]*?```", lambda m: re.sub(r"```\w*\n?", "", m.group()), text
|
|
)
|
|
# Inline code
|
|
text = re.sub(r"`([^`]+)`", r"\1", text)
|
|
# Images
|
|
text = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"\1", text)
|
|
# Links — keep the link text
|
|
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
|
# Bold/italic (order matters: bold+italic first)
|
|
text = re.sub(r"\*\*\*(.+?)\*\*\*", r"\1", text)
|
|
text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
|
|
text = re.sub(r"\*(.+?)\*", r"\1", text)
|
|
text = re.sub(r"___(.+?)___", r"\1", text)
|
|
text = re.sub(r"__(.+?)__", r"\1", text)
|
|
text = re.sub(r"_(.+?)_", r"\1", text)
|
|
# Headers
|
|
text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE)
|
|
# Horizontal rules
|
|
text = re.sub(r"^[-*_]{3,}\s*$", "", text, flags=re.MULTILINE)
|
|
# Bullet lists — remove the bullet marker
|
|
text = re.sub(r"^[\s]*[-*+]\s+", "", text, flags=re.MULTILINE)
|
|
# Numbered lists — remove the number marker
|
|
text = re.sub(r"^[\s]*\d+\.\s+", "", text, flags=re.MULTILINE)
|
|
# Blockquotes
|
|
text = re.sub(r"^>\s?", "", text, flags=re.MULTILINE)
|
|
# Collapse multiple blank lines
|
|
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
return text.strip()
|