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:
@@ -47,11 +47,27 @@ async def get_the_only_conversation() -> Conversation:
|
|||||||
|
|
||||||
async def get_conversation_for_user(user: blueprints.users.models.User) -> Conversation:
|
async def get_conversation_for_user(user: blueprints.users.models.User) -> Conversation:
|
||||||
try:
|
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:
|
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:
|
async def get_conversation_by_id(id: str) -> Conversation:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class Conversation(Model):
|
|||||||
user: fields.ForeignKeyRelation = fields.ForeignKeyField(
|
user: fields.ForeignKeyRelation = fields.ForeignKeyField(
|
||||||
"models.User", related_name="conversations", null=True
|
"models.User", related_name="conversations", null=True
|
||||||
)
|
)
|
||||||
|
channel = fields.CharField(max_length=20, default="web", null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = "conversations"
|
table = "conversations"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from quart import Blueprint, request
|
|||||||
|
|
||||||
from blueprints.users.models import User
|
from blueprints.users.models import User
|
||||||
from blueprints.conversation.logic import (
|
from blueprints.conversation.logic import (
|
||||||
get_conversation_for_user,
|
get_conversation_for_channel,
|
||||||
add_message_to_conversation,
|
add_message_to_conversation,
|
||||||
)
|
)
|
||||||
from blueprints.conversation.agents import main_agent
|
from blueprints.conversation.agents import main_agent
|
||||||
@@ -176,7 +176,7 @@ async def webhook():
|
|||||||
|
|
||||||
# Get or create conversation
|
# Get or create conversation
|
||||||
try:
|
try:
|
||||||
conversation = await get_conversation_for_user(user=user)
|
conversation = await get_conversation_for_channel(user=user, channel="email")
|
||||||
await conversation.fetch_related("messages")
|
await conversation.fetch_related("messages")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get conversation for user {user.username}: {e}")
|
logger.error(f"Failed to get conversation for user {user.username}: {e}")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from quart import Blueprint, request, jsonify
|
|||||||
|
|
||||||
from blueprints.users.models import User
|
from blueprints.users.models import User
|
||||||
from blueprints.conversation.logic import (
|
from blueprints.conversation.logic import (
|
||||||
get_conversation_for_user,
|
get_conversation_for_channel,
|
||||||
add_message_to_conversation,
|
add_message_to_conversation,
|
||||||
)
|
)
|
||||||
from blueprints.conversation.agents import main_agent
|
from blueprints.conversation.agents import main_agent
|
||||||
@@ -178,7 +178,7 @@ async def webhook():
|
|||||||
|
|
||||||
# Get or create conversation
|
# Get or create conversation
|
||||||
try:
|
try:
|
||||||
conversation = await get_conversation_for_user(user=user)
|
conversation = await get_conversation_for_channel(user=user, channel="imessage")
|
||||||
await conversation.fetch_related("messages")
|
await conversation.fetch_related("messages")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get conversation for user {user.username}: {e}")
|
logger.error(f"Failed to get conversation for user {user.username}: {e}")
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
|
||||||
import functools
|
import functools
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
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.request_validator import RequestValidator
|
||||||
from twilio.twiml.messaging_response import MessagingResponse
|
from twilio.twiml.messaging_response import MessagingResponse
|
||||||
|
|
||||||
from blueprints.users.models import User
|
from blueprints.users.models import User
|
||||||
from blueprints.conversation.logic import (
|
from blueprints.conversation.logic import (
|
||||||
get_conversation_for_user,
|
get_conversation_for_channel,
|
||||||
add_message_to_conversation,
|
add_message_to_conversation,
|
||||||
get_conversation_transcript,
|
|
||||||
)
|
)
|
||||||
from blueprints.conversation.agents import main_agent
|
from blueprints.conversation.agents import main_agent
|
||||||
from blueprints.conversation.prompts import SIMBA_SYSTEM_PROMPT
|
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.
|
so the validated URL matches what Twilio signed against.
|
||||||
Set TWILIO_SIGNATURE_VALIDATION=false to disable in development.
|
Set TWILIO_SIGNATURE_VALIDATION=false to disable in development.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
async def decorated_function(*args, **kwargs):
|
async def decorated_function(*args, **kwargs):
|
||||||
if os.getenv("TWILIO_SIGNATURE_VALIDATION", "true").lower() == "false":
|
if os.getenv("TWILIO_SIGNATURE_VALIDATION", "true").lower() == "false":
|
||||||
@@ -94,6 +93,7 @@ def validate_twilio_request(f):
|
|||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
return await f(*args, **kwargs)
|
return await f(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
@@ -108,7 +108,11 @@ async def webhook():
|
|||||||
body = form_data.get("Body")
|
body = form_data.get("Body")
|
||||||
|
|
||||||
if not from_number or not 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
|
# Strip whitespace and check for empty body
|
||||||
body = body.strip()
|
body = body.strip()
|
||||||
@@ -118,12 +122,16 @@ async def webhook():
|
|||||||
# Rate limiting
|
# Rate limiting
|
||||||
if not _check_rate_limit(from_number):
|
if not _check_rate_limit(from_number):
|
||||||
logger.warning(f"Rate limit exceeded for {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
|
# Truncate overly long messages
|
||||||
if len(body) > MAX_MESSAGE_LENGTH:
|
if len(body) > MAX_MESSAGE_LENGTH:
|
||||||
body = 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]}")
|
logger.info(f"Received WhatsApp message from {from_number}: {body[:100]}")
|
||||||
|
|
||||||
@@ -143,16 +151,18 @@ async def webhook():
|
|||||||
username=username,
|
username=username,
|
||||||
email=f"{username}@whatsapp.simbarag.local",
|
email=f"{username}@whatsapp.simbarag.local",
|
||||||
whatsapp_number=from_number,
|
whatsapp_number=from_number,
|
||||||
auth_provider="whatsapp"
|
auth_provider="whatsapp",
|
||||||
)
|
)
|
||||||
logger.info(f"Created new user for WhatsApp: {username}")
|
logger.info(f"Created new user for WhatsApp: {username}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to create user for {from_number}: {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
|
# Get or create a conversation for this user
|
||||||
try:
|
try:
|
||||||
conversation = await get_conversation_for_user(user=user)
|
conversation = await get_conversation_for_channel(user=user, channel="whatsapp")
|
||||||
await conversation.fetch_related("messages")
|
await conversation.fetch_related("messages")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get conversation for user {user.username}: {e}")
|
logger.error(f"Failed to get conversation for user {user.username}: {e}")
|
||||||
@@ -166,9 +176,6 @@ async def webhook():
|
|||||||
user=user,
|
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
|
# Build messages payload for LangChain agent with system prompt and conversation history
|
||||||
try:
|
try:
|
||||||
# Get last 10 messages for conversation history
|
# Get last 10 messages for conversation history
|
||||||
|
|||||||
Reference in New Issue
Block a user