Add channel-scoped conversations for iMessage, WhatsApp, and email

Revert get_conversation_for_user to use Conversation.get() with
MultipleObjectsReturned fallback. Add channel field to Conversation
model and get_conversation_for_channel helper so each messaging
channel gets its own isolated conversation per user.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 19:39:39 -04:00
parent a693874662
commit 3ba93c55f4
5 changed files with 45 additions and 21 deletions
+19 -3
View File
@@ -47,11 +47,27 @@ async def get_the_only_conversation() -> Conversation:
async def get_conversation_for_user(user: blueprints.users.models.User) -> Conversation:
try:
return await Conversation.get(user=user)
conversation = await Conversation.get(user=user)
except tortoise.exceptions.MultipleObjectsReturned:
conversation = (
await Conversation.filter(user=user).order_by("created_at").first()
)
except tortoise.exceptions.DoesNotExist:
await Conversation.get_or_create(name=f"{user.username}'s chat", user=user)
conversation = await Conversation.create(
name=f"{user.username}'s chat", user=user
)
return conversation
return await Conversation.get(user=user)
async def get_conversation_for_channel(
user: blueprints.users.models.User, channel: str
) -> Conversation:
conversation = await Conversation.filter(user=user, channel=channel).first()
if conversation is None:
conversation = await Conversation.create(
name=f"{user.username}'s {channel} chat", user=user, channel=channel
)
return conversation
async def get_conversation_by_id(id: str) -> Conversation:
+1
View File
@@ -21,6 +21,7 @@ class Conversation(Model):
user: fields.ForeignKeyRelation = fields.ForeignKeyField(
"models.User", related_name="conversations", null=True
)
channel = fields.CharField(max_length=20, default="web", null=True)
class Meta:
table = "conversations"
+2 -2
View File
@@ -11,7 +11,7 @@ from quart import Blueprint, request
from blueprints.users.models import User
from blueprints.conversation.logic import (
get_conversation_for_user,
get_conversation_for_channel,
add_message_to_conversation,
)
from blueprints.conversation.agents import main_agent
@@ -176,7 +176,7 @@ async def webhook():
# Get or create conversation
try:
conversation = await get_conversation_for_user(user=user)
conversation = await get_conversation_for_channel(user=user, channel="email")
await conversation.fetch_related("messages")
except Exception as e:
logger.error(f"Failed to get conversation for user {user.username}: {e}")
+2 -2
View File
@@ -10,7 +10,7 @@ from quart import Blueprint, request, jsonify
from blueprints.users.models import User
from blueprints.conversation.logic import (
get_conversation_for_user,
get_conversation_for_channel,
add_message_to_conversation,
)
from blueprints.conversation.agents import main_agent
@@ -178,7 +178,7 @@ async def webhook():
# Get or create conversation
try:
conversation = await get_conversation_for_user(user=user)
conversation = await get_conversation_for_channel(user=user, channel="imessage")
await conversation.fetch_related("messages")
except Exception as e:
logger.error(f"Failed to get conversation for user {user.username}: {e}")
+20 -13
View File
@@ -1,18 +1,16 @@
import os
import logging
import asyncio
import functools
import time
from collections import defaultdict
from quart import Blueprint, request, jsonify, abort
from quart import Blueprint, request, abort
from twilio.request_validator import RequestValidator
from twilio.twiml.messaging_response import MessagingResponse
from blueprints.users.models import User
from blueprints.conversation.logic import (
get_conversation_for_user,
get_conversation_for_channel,
add_message_to_conversation,
get_conversation_transcript,
)
from blueprints.conversation.agents import main_agent
from blueprints.conversation.prompts import SIMBA_SYSTEM_PROMPT
@@ -69,6 +67,7 @@ def validate_twilio_request(f):
so the validated URL matches what Twilio signed against.
Set TWILIO_SIGNATURE_VALIDATION=false to disable in development.
"""
@functools.wraps(f)
async def decorated_function(*args, **kwargs):
if os.getenv("TWILIO_SIGNATURE_VALIDATION", "true").lower() == "false":
@@ -94,6 +93,7 @@ def validate_twilio_request(f):
abort(403)
return await f(*args, **kwargs)
return decorated_function
@@ -108,7 +108,11 @@ async def webhook():
body = form_data.get("Body")
if not from_number or not body:
return _twiml_response("Invalid message received.") if from_number else ("Missing From or Body", 400)
return (
_twiml_response("Invalid message received.")
if from_number
else ("Missing From or Body", 400)
)
# Strip whitespace and check for empty body
body = body.strip()
@@ -118,12 +122,16 @@ async def webhook():
# Rate limiting
if not _check_rate_limit(from_number):
logger.warning(f"Rate limit exceeded for {from_number}")
return _twiml_response("You're sending messages too quickly. Please wait a moment and try again.")
return _twiml_response(
"You're sending messages too quickly. Please wait a moment and try again."
)
# Truncate overly long messages
if len(body) > MAX_MESSAGE_LENGTH:
body = body[:MAX_MESSAGE_LENGTH]
logger.info(f"Truncated long message from {from_number} to {MAX_MESSAGE_LENGTH} chars")
logger.info(
f"Truncated long message from {from_number} to {MAX_MESSAGE_LENGTH} chars"
)
logger.info(f"Received WhatsApp message from {from_number}: {body[:100]}")
@@ -143,16 +151,18 @@ async def webhook():
username=username,
email=f"{username}@whatsapp.simbarag.local",
whatsapp_number=from_number,
auth_provider="whatsapp"
auth_provider="whatsapp",
)
logger.info(f"Created new user for WhatsApp: {username}")
except Exception as e:
logger.error(f"Failed to create user for {from_number}: {e}")
return _twiml_response("Sorry, something went wrong setting up your account. Please try again later.")
return _twiml_response(
"Sorry, something went wrong setting up your account. Please try again later."
)
# Get or create a conversation for this user
try:
conversation = await get_conversation_for_user(user=user)
conversation = await get_conversation_for_channel(user=user, channel="whatsapp")
await conversation.fetch_related("messages")
except Exception as e:
logger.error(f"Failed to get conversation for user {user.username}: {e}")
@@ -166,9 +176,6 @@ async def webhook():
user=user,
)
# Get transcript for context
transcript = await get_conversation_transcript(user=user, conversation=conversation)
# Build messages payload for LangChain agent with system prompt and conversation history
try:
# Get last 10 messages for conversation history