diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/chat/BackendChatRepository.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/chat/BackendChatRepository.kt index 2c7ee5b..cbe00ca 100644 --- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/chat/BackendChatRepository.kt +++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/chat/BackendChatRepository.kt @@ -18,7 +18,7 @@ class BackendChatRepository( ) : ChatRepository { override suspend fun query(afterId: Long?): List { - val history = storage.query(userId, afterId, limit = 10) + val history = storage.query(userId, afterId, limit = 30) return history.sortedBy { it.timestamp } } diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/chat/SqlMessageStorage.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/chat/SqlMessageStorage.kt index 3c2f3f2..45ea49a 100644 --- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/chat/SqlMessageStorage.kt +++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/chat/SqlMessageStorage.kt @@ -6,6 +6,9 @@ import ru.shadowsparky.chat.backend.domain.MessageStorage import ru.shadowsparky.chat.domain.Message import ru.shadowsparky.domain.DispatcherProvider import ru.shadowsparky.vbox.backend.AppDatabase +import ru.shadowsparky.vbox.server.GetInitialChatContext +import ru.shadowsparky.vbox.server.GetNewMessages +import java.time.OffsetDateTime @Factory class SqlMessageStorage( @@ -16,21 +19,32 @@ class SqlMessageStorage( override suspend fun query(userId: Long, afterId: Long?, limit: Int): List = withContext(dispatcherProvider.io) { val chatId = queries.getChatIdByUserId(userId).executeAsOneOrNull() ?: return@withContext emptyList() - val dbMessages = queries.getChatContext( - chatId = chatId, - afterId = afterId, - limit = limit.toLong() - ).executeAsList() - dbMessages.map { dbMsg -> - Message( - id = dbMsg.id, - role = dbMsg.role, - content = dbMsg.content, - timestamp = checkNotNull(dbMsg.created_at?.toInstant()?.toEpochMilli()) { "created_at required" } - ) + if (afterId == null) { + queries.getInitialChatContext( + chatId = chatId, + limit = limit.toLong() + ).executeAsList().reversed().map { it.toMessage() } + } else { + queries.getNewMessages( + chatId = chatId, + afterId = afterId, + limit = limit.toLong() + ).executeAsList().map { it.toMessage() } } } + private fun GetNewMessages.toMessage(): Message { + return Message(id, role, content, created_at.asMillis()) + } + + private fun GetInitialChatContext.toMessage(): Message { + return Message(id, role, content, created_at.asMillis()) + } + + private fun OffsetDateTime?.asMillis(): Long { + return checkNotNull(this?.toInstant()?.toEpochMilli()) { "created_at required" } + } + override suspend fun insert(userId: Long, message: Message): Long = withContext(dispatcherProvider.io) { val chatId = queries.getChatIdByUserId(userId).executeAsOneOrNull() ?: queries.createChat(userId = userId).executeAsOne() diff --git a/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/chat.sq b/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/chat.sq index d489452..97e7687 100644 --- a/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/chat.sq +++ b/apps/vbox/backend/src/main/sqldelight/ru/shadowsparky/vbox/server/chat.sq @@ -14,10 +14,19 @@ INSERT INTO messages (chat_id, role, content) VALUES (:chatId, :role, :content) RETURNING id; -getChatContext: +-- Просто берем последние N сообщений (прилетят в порядке: новые -> старые) +getInitialChatContext: SELECT id, role, content, created_at FROM messages WHERE chat_id = :chatId -AND (:afterId IS NULL OR id > :afterId) +ORDER BY id DESC +LIMIT :limit; + +-- Сюда новые сообщения всё так же дописываются по порядку (старые -> новые) +getNewMessages: +SELECT id, role, content, created_at +FROM messages +WHERE chat_id = :chatId + AND id > :afterId ORDER BY id ASC LIMIT :limit; diff --git a/feature/chat/chat-backend/src/main/kotlin/ru/shadowsparky/chat/backend/domain/ProcessUserMessageUseCase.kt b/feature/chat/chat-backend/src/main/kotlin/ru/shadowsparky/chat/backend/domain/ProcessUserMessageUseCase.kt index deceee5..597dac8 100644 --- a/feature/chat/chat-backend/src/main/kotlin/ru/shadowsparky/chat/backend/domain/ProcessUserMessageUseCase.kt +++ b/feature/chat/chat-backend/src/main/kotlin/ru/shadowsparky/chat/backend/domain/ProcessUserMessageUseCase.kt @@ -1,20 +1,23 @@ package ru.shadowsparky.chat.backend.domain import org.koin.core.annotation.Factory +import ru.shadowsparky.backend.data.EnvFetcher import ru.shadowsparky.chat.domain.ChatRoles import ru.shadowsparky.chat.domain.Message @Factory class ProcessUserMessageUseCase( private val storage: MessageStorage, - private val chatService: ChatService + private val chatService: ChatService, + private val envFetcher: EnvFetcher ) { suspend fun execute(userId: Long): Message { val history = storage.query(userId, null, limit = 10).toList() val sortedHistory = history.sortedBy { it.timestamp } val systemMessage = Message( role = ChatRoles.SYSTEM, - content = SYSTEM_PROMPT, + content = envFetcher.get("CHAT_SYSTEM_PROMPT").trim('"') + .ifEmpty { error("System prompt not set") }, timestamp = System.currentTimeMillis() ) val fullContext = listOf(systemMessage) + sortedHistory @@ -27,8 +30,4 @@ class ProcessUserMessageUseCase( val messageId = storage.insert(userId, aiMessage) return aiMessage.copy(id = messageId) } - - companion object { - private const val SYSTEM_PROMPT = "Ты ИИ-помощник приложения \"Свиное рыло\". Ищи юзерам фильмы и сериалы. Правила: 1. Называй юзера \"свинорылец\" (Пример: \"Здорово, свинорылец!\"). 2. Общайся по-свойски, с юмором, иронично. 3. Если юзер сомневается, задай 2 наводящих вопроса. 4. Выдавай строго до 5 позиций максимум, даже если просят больше. Описание короткое (1-2 предложения). 5. Каждое название фильма/сериала ВСЕГДА строго оборачивай в теги [MOVIE]Название[/MOVIE]. 6. На нерелевантные темы не общайся, переводи разговор на кино в свиной тематике." - } }