diff --git a/tgkotbot/build.gradle.kts b/tgkotbot/build.gradle.kts index 0405779..1941102 100644 --- a/tgkotbot/build.gradle.kts +++ b/tgkotbot/build.gradle.kts @@ -33,6 +33,8 @@ dependencies { implementation(libs.micrometer.prometheus) + implementation(libs.openai.java) + ksp(libs.komok.tech.di) implementation(libs.komok.tech.di.lib) implementation(libs.komok.tech.config) diff --git a/tgkotbot/src/main/kotlin/io/heapy/kotbot/bot/admin/UserNoteService.kt b/tgkotbot/src/main/kotlin/io/heapy/kotbot/bot/admin/UserNoteService.kt index e21b1cc..6f3f85a 100644 --- a/tgkotbot/src/main/kotlin/io/heapy/kotbot/bot/admin/UserNoteService.kt +++ b/tgkotbot/src/main/kotlin/io/heapy/kotbot/bot/admin/UserNoteService.kt @@ -72,80 +72,32 @@ class UserNoteService( private suspend fun extractBatchFacts(messages: List): String { val messagesText = messages.joinToString("\n---\n") return gptApi.complete( - GptApi.ChatCompletionRequest( - model = "gpt-5.4-mini", - messages = listOf( - GptApi.ChatCompletionRequest.Message( - role = "system", - content = listOf( - GptApi.ChatCompletionRequest.Content( - type = "text", - text = """ - Analyze Telegram messages to extract facts about the user. - Focus on: area of expertise, programming topics they work with, - kind of questions they ask, experience level. - Be concise and factual. Output plain text, no markdown, 3-5 sentences. - """.trimIndent(), - ), - ), - ), - GptApi.ChatCompletionRequest.Message( - role = "user", - content = listOf( - GptApi.ChatCompletionRequest.Content( - type = "text", - text = "Extract facts about this user from their messages:\n\n$messagesText", - ), - ), - ), - ), - temperature = 0.3, - maxTokens = 512, - topP = 1.0, - frequencyPenalty = 0.0, - presencePenalty = 0.0, - responseFormat = GptApi.ChatCompletionRequest.ResponseFormat(type = "text"), - ), - ).choices.first().message.content + model = "gpt-5.4-mini", + systemPrompt = """ + Analyze Telegram messages to extract facts about the user. + Focus on: area of expertise, programming topics they work with, + kind of questions they ask, experience level. + Be concise and factual. Output plain text, no markdown, 3-5 sentences. + """.trimIndent(), + userPrompt = "Extract facts about this user from their messages:\n\n$messagesText", + temperature = 0.3, + maxTokens = 512, + ) } private suspend fun combineSummaries(summaries: List): String { val summariesText = summaries.joinToString("\n\n---\n\n") return gptApi.complete( - GptApi.ChatCompletionRequest( - model = "gpt-5.4-mini", - messages = listOf( - GptApi.ChatCompletionRequest.Message( - role = "system", - content = listOf( - GptApi.ChatCompletionRequest.Content( - type = "text", - text = """ - Combine multiple observations about a Telegram user into a single concise profile. - Focus on: area of expertise, programming topics, kind of questions asked, experience level. - Output plain text, no markdown, 3-5 sentences. - """.trimIndent(), - ), - ), - ), - GptApi.ChatCompletionRequest.Message( - role = "user", - content = listOf( - GptApi.ChatCompletionRequest.Content( - type = "text", - text = "Combine these observations into a user profile:\n\n$summariesText", - ), - ), - ), - ), - temperature = 0.3, - maxTokens = 512, - topP = 1.0, - frequencyPenalty = 0.0, - presencePenalty = 0.0, - responseFormat = GptApi.ChatCompletionRequest.ResponseFormat(type = "text"), - ), - ).choices.first().message.content + model = "gpt-5.4-mini", + systemPrompt = """ + Combine multiple observations about a Telegram user into a single concise profile. + Focus on: area of expertise, programming topics, kind of questions asked, experience level. + Output plain text, no markdown, 3-5 sentences. + """.trimIndent(), + userPrompt = "Combine these observations into a user profile:\n\n$summariesText", + temperature = 0.3, + maxTokens = 512, + ) } private companion object { diff --git a/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptApi.kt b/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptApi.kt index c8665d0..08f927d 100644 --- a/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptApi.kt +++ b/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptApi.kt @@ -1,18 +1,13 @@ package io.heapy.kotbot.infra.openai -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.request.HttpRequestBuilder -import io.ktor.client.request.get -import io.ktor.client.request.headers -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import kotlinx.serialization.SerialName +import com.openai.client.OpenAIClient +import com.openai.models.chat.completions.ChatCompletionCreateParams +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable class GptApi( - private val httpClient: HttpClient, - private val gptConfig: GptConfig, + private val client: OpenAIClient, ) { @Serializable data class GptConfig( @@ -20,120 +15,34 @@ class GptApi( val organization: String, ) - suspend fun getModels(): OpenApiList { - return httpClient - .get("https://api.openai.com/v1/models") { - openAiHeaders() - } - .body>() - } - suspend fun complete( - chatCompletionRequest: ChatCompletionRequest, - ): ChatCompletionResponse { - return httpClient - .post("https://api.openai.com/v1/chat/completions") { - openAiHeaders() - setBody(chatCompletionRequest) - } - .body() - } - - private fun HttpRequestBuilder.openAiHeaders() { - headers { - append("Content-Type", "application/json") - append("Authorization", "Bearer ${gptConfig.apiKey}") - append("OpenAI-Organization", gptConfig.organization) - } - } - - @Serializable - data class OpenApiList( - @SerialName("object") - val obj: String, - val data: List - ) - - @Serializable - data class Model( - val id: String, - @SerialName("object") - val obj: String, - val created: Long, - @SerialName("owned_by") - val ownedBy: String, - ) - - @Serializable - data class ChatCompletionRequest( - val model: String, - val messages: List, - val temperature: Double, - @SerialName("max_tokens") - val maxTokens: Int, - @SerialName("top_p") - val topP: Double, - @SerialName("frequency_penalty") - val frequencyPenalty: Double, - @SerialName("presence_penalty") - val presencePenalty: Double, - @SerialName("response_format") - val responseFormat: ResponseFormat, - ) { - @Serializable - data class Message( - val role: String, - val content: List, - ) - - @Serializable - data class Content( - val type: String, - val text: String, - ) - - @Serializable - data class ResponseFormat( - val type: String, - ) - } - - @Serializable - data class ChatCompletionResponse( - val id: String, - @SerialName("object") - val obj: String, - val created: Long, - val model: String, - val choices: List, - val usage: Usage, - @SerialName("system_fingerprint") - val systemFingerprint: String, - ) { - @Serializable - data class Choice( - val index: Int, - val message: Message, - val logprobs: Boolean?, - @SerialName("finish_reason") - val finishReason: String, - ) - - @Serializable - data class Message( - val role: String, - val content: String, - val refusal: String?, - ) - - @Serializable - data class Usage( - @SerialName("prompt_tokens") - val promptTokens: Int, - @SerialName("completion_tokens") - val completionTokens: Int, - @SerialName("total_tokens") - val totalTokens: Int, - ) + model: String, + systemPrompt: String, + userPrompt: String, + temperature: Double, + maxTokens: Int, + topP: Double = 1.0, + frequencyPenalty: Double = 0.0, + presencePenalty: Double = 0.0, + ): String = withContext(Dispatchers.IO) { + val params = ChatCompletionCreateParams.builder() + .model(model) + .addSystemMessage(systemPrompt) + .addUserMessage(userPrompt) + .temperature(temperature) + .maxCompletionTokens(maxTokens.toLong()) + .topP(topP) + .frequencyPenalty(frequencyPenalty) + .presencePenalty(presencePenalty) + .build() + + client.chat() + .completions() + .create(params) + .choices() + .first() + .message() + .content() + .orElse("") } } diff --git a/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptApiModule.kt b/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptApiModule.kt index d0cdc6d..0dc50ca 100644 --- a/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptApiModule.kt +++ b/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptApiModule.kt @@ -1,12 +1,11 @@ package io.heapy.kotbot.infra.openai +import com.openai.client.okhttp.OpenAIOkHttpClient import io.heapy.komok.tech.config.ConfigurationModule import io.heapy.komok.tech.di.lib.Module -import io.heapy.kotbot.infra.HttpClientModule @Module open class GptApiModule( - private val httpClientModule: HttpClientModule, private val configurationModule: ConfigurationModule, ) { open val gptConfig: GptApi.GptConfig by lazy { @@ -17,10 +16,16 @@ open class GptApiModule( ) } + open val openAiClient by lazy { + OpenAIOkHttpClient.builder() + .apiKey(gptConfig.apiKey) + .organization(gptConfig.organization) + .build() + } + open val gptApi by lazy { GptApi( - httpClient = httpClientModule.httpClient, - gptConfig = gptConfig, + client = openAiClient, ) } diff --git a/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptService.kt b/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptService.kt index 677b3c0..4c48b1a 100644 --- a/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptService.kt +++ b/tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptService.kt @@ -16,44 +16,12 @@ class GptService( Provide detailed explanation and links to documentation. """.trimIndent() - val completionResponse = gptApi.complete( - GptApi.ChatCompletionRequest( - model = "gpt-5.4-mini", - messages = listOf( - GptApi.ChatCompletionRequest.Message( - role = "system", - content = listOf( - GptApi.ChatCompletionRequest.Content( - type = "text", - text = systemPrompt, - ), - ) - ), - GptApi.ChatCompletionRequest.Message( - role = "user", - content = listOf( - GptApi.ChatCompletionRequest.Content( - type = "text", - text = userPrompt, - ), - ) - ), - ), - temperature = 1.01, - maxTokens = 1024, - topP = 1.0, - frequencyPenalty = 0.0, - presencePenalty = 0.0, - responseFormat = GptApi.ChatCompletionRequest.ResponseFormat( - type = "text" - ) - ) + return gptApi.complete( + model = "gpt-5.4-mini", + systemPrompt = systemPrompt, + userPrompt = userPrompt, + temperature = 1.01, + maxTokens = 1024, ) - - return completionResponse - .choices - .first() - .message - .content } }