140 lines
4.5 KiB
Python
140 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from django.db.models import Q, QuerySet
|
|
from django.utils import timezone
|
|
|
|
from .llm import LLMConfigurationError, LLMRequestError, generate_reply, stream_reply
|
|
from .models import Conversation, Message
|
|
|
|
|
|
def list_conversations(user, search: str = "") -> QuerySet[Conversation]:
|
|
"""Returns a user's conversations, optionally filtered by title or content."""
|
|
|
|
conversations = Conversation.objects.filter(user=user)
|
|
if not search:
|
|
return conversations
|
|
|
|
return conversations.filter(
|
|
Q(title__icontains=search) | Q(messages__content__icontains=search)
|
|
).distinct()
|
|
|
|
|
|
def get_conversation_for_user(user, conversation_id: int | None) -> Conversation | None:
|
|
"""Loads a conversation only when it belongs to the current user."""
|
|
|
|
if not conversation_id:
|
|
return None
|
|
return Conversation.objects.filter(user=user, pk=conversation_id).first()
|
|
|
|
|
|
def create_conversation(user) -> Conversation:
|
|
"""Creates an empty conversation that can immediately accept messages."""
|
|
|
|
now = timezone.localtime()
|
|
return Conversation.objects.create(
|
|
user=user,
|
|
title=f"新对话 {now.strftime('%m-%d %H:%M')}",
|
|
)
|
|
|
|
|
|
def append_user_message(conversation: Conversation, content: str) -> Message:
|
|
"""Appends a user message and updates the conversation title from the first prompt."""
|
|
|
|
message = Message.objects.create(
|
|
conversation=conversation,
|
|
role=Message.Role.USER,
|
|
content=content.strip(),
|
|
)
|
|
|
|
if conversation.messages.filter(role=Message.Role.USER).count() == 1:
|
|
conversation.title = build_conversation_title(content)
|
|
conversation.save(update_fields=["title", "updated_at"])
|
|
|
|
return message
|
|
|
|
|
|
def append_assistant_message(conversation: Conversation, content: str) -> Message:
|
|
"""Appends the deterministic assistant reply."""
|
|
|
|
return Message.objects.create(
|
|
conversation=conversation,
|
|
role=Message.Role.ASSISTANT,
|
|
content=content,
|
|
)
|
|
|
|
|
|
def send_message(conversation: Conversation, content: str) -> tuple[Message, Message]:
|
|
"""Stores one user message and one provider-backed assistant reply."""
|
|
|
|
user_message = append_user_message(conversation, content)
|
|
try:
|
|
reply_content = generate_reply(conversation, content)
|
|
except (LLMConfigurationError, LLMRequestError) as exc:
|
|
reply_content = f"模型调用失败:{exc}"
|
|
|
|
assistant_message = append_assistant_message(conversation, reply_content)
|
|
|
|
if conversation.title.startswith("新对话"):
|
|
conversation.title = build_conversation_title(content)
|
|
conversation.save(update_fields=["title", "updated_at"])
|
|
|
|
return user_message, assistant_message
|
|
|
|
|
|
def stream_message(conversation: Conversation, content: str):
|
|
"""Yields SSE events while collecting a streamed assistant reply."""
|
|
|
|
user_message = append_user_message(conversation, content)
|
|
assistant_parts: list[str] = []
|
|
|
|
yield sse_event(
|
|
"meta",
|
|
{
|
|
"conversation_id": conversation.pk,
|
|
"title": conversation.title or build_conversation_title(content),
|
|
"user_message_id": user_message.pk,
|
|
"user_message": user_message.content,
|
|
},
|
|
)
|
|
|
|
try:
|
|
for chunk in stream_reply(conversation, content):
|
|
assistant_parts.append(chunk)
|
|
yield sse_event("chunk", {"delta": chunk})
|
|
except (LLMConfigurationError, LLMRequestError) as exc:
|
|
fallback = f"模型调用失败:{exc}"
|
|
assistant_parts = [fallback]
|
|
yield sse_event("error", {"message": fallback})
|
|
|
|
assistant_message = append_assistant_message(conversation, "".join(assistant_parts).strip())
|
|
|
|
if conversation.title.startswith("新对话"):
|
|
conversation.title = build_conversation_title(content)
|
|
conversation.save(update_fields=["title", "updated_at"])
|
|
|
|
yield sse_event(
|
|
"done",
|
|
{
|
|
"assistant_message_id": assistant_message.pk,
|
|
"conversation_id": conversation.pk,
|
|
"title": conversation.title,
|
|
},
|
|
)
|
|
|
|
|
|
def build_conversation_title(content: str) -> str:
|
|
"""Creates a concise title from the first user message."""
|
|
|
|
normalized = " ".join(content.strip().split())
|
|
if not normalized:
|
|
return "新对话"
|
|
return normalized[:24]
|
|
|
|
|
|
def sse_event(event_name: str, payload: dict[str, object]) -> str:
|
|
"""Formats one server-sent event frame."""
|
|
|
|
return f"event: {event_name}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"
|