Add iMessage threading, typing indicators, and fix tool-use response extraction

- 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>
This commit is contained in:
2026-09-15 23:10:50 -04:00
parent 0478ee7568
commit 7474055c97
6 changed files with 149 additions and 21 deletions
+21
View File
@@ -1,5 +1,26 @@
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."""