backend part
This commit is contained in:
@@ -21,6 +21,8 @@ dependencies {
|
||||
implementation(project(":libs:base"))
|
||||
implementation(project(":libs:http-client"))
|
||||
implementation(project(":feature:updater:updater-common"))
|
||||
implementation(project(":feature:chat:chat-common"))
|
||||
implementation(project(":feature:chat:chat-backend"))
|
||||
|
||||
implementation(libs.ktor.server.core.jvm)
|
||||
implementation(libs.ktor.server.host.common.jvm)
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package ru.shadowsparky.vbox.backend.data.chat
|
||||
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.chat.backend.domain.MessageStorage
|
||||
import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase
|
||||
import ru.shadowsparky.chat.domain.ChatRepository
|
||||
import ru.shadowsparky.chat.domain.Message
|
||||
|
||||
@Factory
|
||||
class BackendChatRepositoryFactory(
|
||||
private val storage: MessageStorage,
|
||||
private val processUserMessageUseCase: ProcessUserMessageUseCase
|
||||
) {
|
||||
fun create(userId: Long): BackendChatRepository {
|
||||
return BackendChatRepository(userId, storage, processUserMessageUseCase)
|
||||
}
|
||||
}
|
||||
|
||||
class BackendChatRepository(
|
||||
private val userId: Long,
|
||||
private val storage: MessageStorage,
|
||||
private val processUserMessageUseCase: ProcessUserMessageUseCase
|
||||
) : ChatRepository {
|
||||
|
||||
override suspend fun query(): List<Message> {
|
||||
val history = storage.query(userId, limit = 10)
|
||||
return history.sortedBy { it.timestamp }
|
||||
}
|
||||
|
||||
override suspend fun send(message: Message) {
|
||||
storage.insert(userId, message)
|
||||
processUserMessageUseCase.execute(userId)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package ru.shadowsparky.vbox.backend.data.chat
|
||||
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.chat.backend.domain.MessageStorage
|
||||
import ru.shadowsparky.chat.domain.Message
|
||||
import ru.shadowsparky.domain.DispatcherProvider
|
||||
import ru.shadowsparky.vbox.backend.AppDatabase
|
||||
|
||||
@Factory
|
||||
class SqlMessageStorage(
|
||||
appDatabase: AppDatabase,
|
||||
private val dispatcherProvider: DispatcherProvider
|
||||
) : MessageStorage {
|
||||
private val queries = appDatabase.chatQueries
|
||||
|
||||
override suspend fun query(userId: Long, limit: Int): List<Message> = withContext(dispatcherProvider.io) {
|
||||
val chatId = queries.getChatIdByUserId(userId).executeAsOneOrNull() ?: return@withContext emptyList()
|
||||
val dbMessages = queries.getChatContext(chatId = chatId, 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" }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun insert(userId: Long, message: Message): Long = withContext(dispatcherProvider.io) {
|
||||
val chatId = queries.getChatIdByUserId(userId).executeAsOneOrNull()
|
||||
?: queries.createChat(userId = userId).executeAsOne()
|
||||
queries.insertMessage(
|
||||
chatId = chatId,
|
||||
role = message.role,
|
||||
content = message.content
|
||||
).executeAsOne()
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import ru.shadowsparky.backend.domain.LoginVerifier
|
||||
import ru.shadowsparky.vbox.backend.data.BackendRemoteEventHandler
|
||||
import ru.shadowsparky.vbox.backend.data.BackendUpdateFetcherFactory
|
||||
import ru.shadowsparky.vbox.backend.data.auth.AuthTokenRepositoryFactory
|
||||
import ru.shadowsparky.vbox.backend.data.chat.BackendChatRepository
|
||||
import ru.shadowsparky.vbox.backend.data.chat.BackendChatRepositoryFactory
|
||||
import ru.shadowsparky.vbox.backend.data.tags.MovieTagRepositoryFactory
|
||||
import ru.shadowsparky.vbox.backend.data.tags.UserTagRepositoryFactory
|
||||
import ru.shadowsparky.vbox.backend.di.factory.RecentlyWatchedRepositoryFactory
|
||||
@@ -24,7 +26,8 @@ class RoutingEntryPoint(
|
||||
val savedMovieFactory: SavedMovieRepositoryFactory,
|
||||
val movieTagFactory: MovieTagRepositoryFactory,
|
||||
val userTagFactory: UserTagRepositoryFactory,
|
||||
val updateFetcherFactory: BackendUpdateFetcherFactory
|
||||
val updateFetcherFactory: BackendUpdateFetcherFactory,
|
||||
val chatRepositoryFactory: BackendChatRepositoryFactory
|
||||
)
|
||||
|
||||
@Single
|
||||
|
||||
+1
@@ -45,5 +45,6 @@ fun Routing.setupAuthMethods(
|
||||
setupSavedMovie(savedMovieFactory)
|
||||
setupTagsRouting(userTagFactory, movieTagFactory)
|
||||
setupUpdates(updateFetcherFactory)
|
||||
setupChat(chatRepositoryFactory)
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package ru.shadowsparky.vbox.backend.presentation.routing
|
||||
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.request.receive
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.post
|
||||
import ru.shadowsparky.vbox.backend.data.chat.BackendChatRepositoryFactory
|
||||
import ru.shadowsparky.vbox.backend.presentation.obtainUserId
|
||||
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
|
||||
|
||||
fun Route.setupChat(chatRepositoryFactory: BackendChatRepositoryFactory) {
|
||||
get("$DYNAMIC_PREFIX/chat/query") {
|
||||
val repository = chatRepositoryFactory.create(call.obtainUserId())
|
||||
call.respond(repository.query())
|
||||
}
|
||||
post("$DYNAMIC_PREFIX/chat/send") {
|
||||
val repository = chatRepositoryFactory.create(call.obtainUserId())
|
||||
repository.send(call.receive())
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
getChatIdByUserId:
|
||||
SELECT id
|
||||
FROM chats
|
||||
WHERE user_id = :userId AND is_active = TRUE
|
||||
LIMIT 1;
|
||||
|
||||
createChat:
|
||||
INSERT INTO chats (user_id)
|
||||
VALUES (:userId)
|
||||
RETURNING id;
|
||||
|
||||
insertMessage:
|
||||
INSERT INTO messages (chat_id, role, content)
|
||||
VALUES (:chatId, :role, :content)
|
||||
RETURNING id;
|
||||
|
||||
getChatContext:
|
||||
SELECT id, role, content, created_at
|
||||
FROM messages
|
||||
WHERE chat_id = :chatId
|
||||
ORDER BY created_at DESC
|
||||
LIMIT :limit;
|
||||
@@ -78,3 +78,19 @@ CREATE TABLE movie_tag (
|
||||
CONSTRAINT movie_tag_movie_fk FOREIGN KEY (movie_id) REFERENCES movie(movie_id),
|
||||
CONSTRAINT movie_tag_user_tag_fk FOREIGN KEY (tag_id) REFERENCES user_tag(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chats (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id BIGINT NOT NULL REFERENCES "user"(user_id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) DEFAULT 'Новый подбор',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
chat_id UUID NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
+5
-20
@@ -10,7 +10,6 @@ import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -19,7 +18,6 @@ import kotlinx.serialization.json.Json
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.chat.backend.domain.ChatService
|
||||
import ru.shadowsparky.chat.domain.Message
|
||||
import ru.shadowsparky.http.domain.HttpException
|
||||
import java.util.UUID
|
||||
|
||||
@Factory
|
||||
@@ -35,28 +33,15 @@ class GigaChatService(
|
||||
val requestBody = GigaChatRequest(
|
||||
messages = messages.map { GigaChatMessageDto(role = it.role, content = it.content) }
|
||||
)
|
||||
val response = try {
|
||||
executeRequest(clientId, staticSessionId, requestBody)
|
||||
} catch (e: HttpException) {
|
||||
if (e.httpCode == HttpStatusCode.Unauthorized.value) {
|
||||
tokenManager.forceRefreshToken()
|
||||
executeRequest(clientId, staticSessionId, requestBody)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return response.choices.firstOrNull()?.message?.content
|
||||
?: error("unexpected response $response")
|
||||
}
|
||||
|
||||
private suspend fun executeRequest(clientId: String, sessionId: String, body: GigaChatRequest): GigaChatResponse {
|
||||
return httpClient.post("https://api.giga.chat/v1/chat/completions") {
|
||||
val response: GigaChatResponse = httpClient.post("https://api.giga.chat/v1/chat/completions") {
|
||||
contentType(ContentType.Application.Json)
|
||||
header("X-Request-ID", UUID.randomUUID().toString())
|
||||
header("X-Session-ID", sessionId)
|
||||
header("X-Session-ID", staticSessionId)
|
||||
header("X-Client-ID", clientId)
|
||||
setBody(body)
|
||||
setBody(requestBody)
|
||||
}.body()
|
||||
return response.choices.firstOrNull()?.message?.content
|
||||
?: error("unexpected response $response")
|
||||
}
|
||||
|
||||
private fun createGigaChatHttpClient(): HttpClient {
|
||||
|
||||
+1
-2
@@ -9,8 +9,7 @@ class ProcessUserMessageUseCase(
|
||||
private val storage: MessageStorage,
|
||||
private val chatService: ChatService
|
||||
) {
|
||||
suspend fun execute(userId: Long, userMessage: Message): Message {
|
||||
storage.insert(userId, userMessage)
|
||||
suspend fun execute(userId: Long): Message {
|
||||
val history = storage.query(userId, limit = 10).toList()
|
||||
val sortedHistory = history.sortedBy { it.timestamp }
|
||||
val systemMessage = Message(
|
||||
|
||||
+2
-5
@@ -1,9 +1,6 @@
|
||||
package ru.shadowsparky.chat.domain
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ChatRepository {
|
||||
val messages: Flow<Message>
|
||||
|
||||
suspend fun sendMessage(message: Message)
|
||||
suspend fun query(): List<Message>
|
||||
suspend fun send(message: Message)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user