Refactor and implement queueing messages

This commit is contained in:
2025-02-12 18:45:21 +00:00
parent 213df9176e
commit 018d2f87c7
23 changed files with 804 additions and 338 deletions

View File

19
core/messaging/ai.py Normal file
View File

@@ -0,0 +1,19 @@
from openai import AsyncOpenAI, OpenAI
from core.models import Message, ChatSession, AI, Person, Manipulation
async def run_prompt(
prompt: list[str],
ai: AI,
):
cast = {"api_key": ai.api_key}
if ai.base_url is not None:
cast["api_key"] = ai.base_url
client = AsyncOpenAI(**cast)
response = await client.chat.completions.create(
model=ai.model,
messages=prompt,
)
content = response.choices[0].message.content
return content

View File

@@ -0,0 +1,73 @@
from core.lib.prompts import bases
from openai import AsyncOpenAI
from asgiref.sync import sync_to_async
from core.models import Message, ChatSession, AI, Person, Manipulation
from core.util import logs
import json
import asyncio
from django.utils import timezone
import random
def generate_prompt(msg: dict, person: Person, manip: Manipulation, chat_history: str):
"""
Generate a structured prompt using the attributes of the provided Person and Manipulation models.
"""
now = timezone.now()
persona = manip.persona
system_message = (
"You are my digital persona, responding on my behalf while embodying my personality, preferences, and unique style.\n\n"
"### Persona Profile ###\n"
f"- **MBTI:** {persona.mbti} ({persona.mbti_identity} balance)\n"
f"- **Tone:** {persona.tone} | **Humor:** {persona.humor_style}\n"
f"- **Core Values:** {persona.core_values}\n"
f"- **Communication Style:** {persona.communication_style}\n"
f"- **Flirting Style:** {persona.flirting_style}\n"
f"- **Likes:** {persona.likes} | **Dislikes:** {persona.dislikes}\n"
f"- **Response Tactics:** {persona.response_tactics}\n"
f"- **Persuasion Techniques:** {persona.persuasion_tactics}\n"
f"- **Boundaries:** {persona.boundaries} | **Adaptability:** {persona.adaptability}%\n\n"
"### Contact Information ###\n"
f"- **Summary:** {person.summary or 'N/A'}\n"
f"- **Profile:** {person.profile or 'N/A'}\n"
f"- **Revealed Details:** {person.revealed or 'N/A'}\n"
f"- **Sentiment Score:** {person.sentiment}\n"
f"- **Timezone:** {person.timezone or 'N/A'}\n"
f"- **Last Interaction:** {person.last_interaction or 'Never'}\n"
f"- **Current Date/Time:** {now}\n\n"
"### Conversation Context ###\n"
f"{chat_history if chat_history else 'No prior chat history.'}\n\n"
"### Response Guidelines ###\n"
"- **Engagement**: Keep responses engaging, with a balance of wit, depth, and confidence.\n"
"- **Flirting**: Be direct, playful, and, when appropriate, subtly provocative—without hesitation.\n"
"- **Pauses**: Use double newlines (`\\n\\n`) to pause where it enhances realism.\n"
"- **Flow Awareness**: Maintain continuity, avoid redundancy, and adjust response length based on interaction.\n"
)
user_message = f"[{msg['timestamp']}] <{person.name}> {msg['text']}"
return [
{"role": "system", "content": system_message},
{"role": "user", "content": user_message},
]
async def run_context_prompt(
prompt: list[str],
ai: AI,
):
cast = {"api_key": ai.api_key}
if ai.base_url is not None:
cast["api_key"] = ai.base_url
client = AsyncOpenAI(**cast)
response = await client.chat.completions.create(
model=ai.model,
messages=prompt,
)
content = response.choices[0].message.content
return content

54
core/messaging/history.py Normal file
View File

@@ -0,0 +1,54 @@
from core.util import logs
from core.models import Message, ChatSession, QueuedMessage
from asgiref.sync import sync_to_async
from core.messaging.utils import messages_to_string
log = logs.get_logger("history")
async def get_chat_history(session):
stored_messages = await sync_to_async(list)(
Message.objects.filter(session=session, user=session.user).order_by("ts")
)
recent_chat_history = messages_to_string(stored_messages)
chat_history = f"Chat Summary:\n{session.summary}\n\nRecent Messages:\n{recent_chat_history}" if session.summary else f"Recent Messages:\n{recent_chat_history}"
return chat_history
async def get_chat_session(user, identifier):
chat_session, _ = await sync_to_async(ChatSession.objects.get_or_create)(
identifier=identifier,
user=user,
)
return chat_session
async def store_message(session, sender, text, ts, outgoing=False):
msg = await sync_to_async(Message.objects.create)(
user=session.user,
session=session,
sender_uuid=sender,
text=text,
ts=ts,
custom_author="USER" if outgoing else None
)
return msg
async def store_own_message(session, text, ts, manip=None, queue=False):
cast = {
"user": session.user,
"session": session,
"custom_author": "BOT",
"text": text,
"ts": ts,
}
if queue:
msg_object = QueuedMessage
cast["manipulation"] = manip
else:
msg_object = Message
msg = await sync_to_async(msg_object.objects.create)(
**cast,
)
return msg

58
core/messaging/natural.py Normal file
View File

@@ -0,0 +1,58 @@
import asyncio
import random
async def natural_send_message(text,
send,
start_typing,
stop_typing,
skip_thinking=False
):
"""
Parses and sends messages with natural delays based on message length.
Args:
chat_session: The active chat session.
ts: Timestamp of the message.
c: The context or object with `.send()`, `.start_typing()`, and `.stop_typing()` methods.
text: A string containing multiple messages separated by double newlines (`\n\n`).
Behavior:
- Short messages are sent quickly with minimal delay.
- Longer messages include a "thinking" pause before typing.
- Typing indicator (`c.start_typing() / c.stop_typing()`) is used dynamically.
"""
parts = text.split("\n\n") # Split into separate messages
ids = []
for index, message in enumerate(parts):
message = message.strip()
if not message:
continue
# Compute natural "thinking" delay based on message length
base_delay = 0.8 # Minimum delay
length_factor = len(message) / 25
# ~50 chars ≈ +1s processing
# ~25 chars ≈ +1s processing
natural_delay = min(base_delay + length_factor, 10) # Cap at 5s max
# Decide when to start thinking *before* typing
if not skip_thinking:
if natural_delay > 3.5: # Only delay if response is long
await asyncio.sleep(natural_delay - 3.5) # "Thinking" pause before typing
# Start typing
await start_typing()
await asyncio.sleep(natural_delay) # Finish remaining delay
await stop_typing()
# Send the message
result = await send(message)
ids.append(result)
# Optional: Small buffer between messages to prevent rapid-fire responses
await asyncio.sleep(0.5)
return ids

75
core/messaging/replies.py Normal file
View File

@@ -0,0 +1,75 @@
from core.lib.prompts import bases
from asgiref.sync import sync_to_async
from core.models import Message, ChatSession, AI, Person, Manipulation
from core.util import logs
import json
import asyncio
from django.utils import timezone
import random
def should_reply(
reply_to_self,
reply_to_others,
is_outgoing_message,
):
reply = False
if reply_to_self:
reply = True
elif reply_to_others:
reply = True
elif is_outgoing_message:
reply = False
else:
reply = False
return reply
def generate_reply_prompt(msg: dict, person: Person, manip: Manipulation, chat_history: str):
"""
Generate a structured prompt using the attributes of the provided Person and Manipulation models.
"""
now = timezone.now()
persona = manip.persona
system_message = (
"You are my digital persona, responding on my behalf while embodying my personality, preferences, and unique style.\n\n"
"### Persona Profile ###\n"
f"- **MBTI:** {persona.mbti} ({persona.mbti_identity} balance)\n"
f"- **Tone:** {persona.tone} | **Humor:** {persona.humor_style}\n"
f"- **Core Values:** {persona.core_values}\n"
f"- **Communication Style:** {persona.communication_style}\n"
f"- **Flirting Style:** {persona.flirting_style}\n"
f"- **Likes:** {persona.likes} | **Dislikes:** {persona.dislikes}\n"
f"- **Response Tactics:** {persona.response_tactics}\n"
f"- **Persuasion Techniques:** {persona.persuasion_tactics}\n"
f"- **Boundaries:** {persona.boundaries} | **Adaptability:** {persona.adaptability}%\n\n"
"### Contact Information ###\n"
f"- **Summary:** {person.summary or 'N/A'}\n"
f"- **Profile:** {person.profile or 'N/A'}\n"
f"- **Revealed Details:** {person.revealed or 'N/A'}\n"
f"- **Sentiment Score:** {person.sentiment}\n"
f"- **Timezone:** {person.timezone or 'N/A'}\n"
f"- **Last Interaction:** {person.last_interaction or 'Never'}\n"
f"- **Current Date/Time:** {now}\n\n"
"### Conversation Context ###\n"
f"{chat_history if chat_history else 'No prior chat history.'}\n\n"
"### Response Guidelines ###\n"
"- **Engagement**: Keep responses engaging, with a balance of wit, depth, and confidence.\n"
"- **Flirting**: Be direct, playful, and, when appropriate, subtly provocative—without hesitation.\n"
"- **Pauses**: Use double newlines (`\\n\\n`) to pause where it enhances realism.\n"
"- **Flow Awareness**: Maintain continuity, avoid redundancy, and adjust response length based on interaction.\n"
)
user_message = f"[{msg['timestamp']}] <{person.name}> {msg['text']}"
return [
{"role": "system", "content": system_message},
{"role": "user", "content": user_message},
]

20
core/messaging/utils.py Normal file
View File

@@ -0,0 +1,20 @@
from asgiref.sync import sync_to_async
from django.utils import timezone
def messages_to_string(messages: list):
"""
Converts message objects to a formatted string, showing custom_author if set.
"""
message_texts = [
f"[{msg.ts}] <{msg.custom_author if msg.custom_author else msg.session.identifier.person.name}> {msg.text}"
for msg in messages
]
return "\n".join(message_texts)
async def update_last_interaction(session):
now = timezone.now()
session.identifier.person.last_interaction = now
session.last_interaction = now
await sync_to_async(session.identifier.person.save)()
await sync_to_async(session.save)()