Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tgkotbot/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,80 +72,32 @@ class UserNoteService(
private suspend fun extractBatchFacts(messages: List<String>): 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>): 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 {
Expand Down
157 changes: 33 additions & 124 deletions tgkotbot/src/main/kotlin/io/heapy/kotbot/infra/openai/GptApi.kt
Original file line number Diff line number Diff line change
@@ -1,139 +1,48 @@
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(
val apiKey: String,
val organization: String,
)

suspend fun getModels(): OpenApiList<Model> {
return httpClient
.get("https://api.openai.com/v1/models") {
openAiHeaders()
}
.body<OpenApiList<Model>>()
}

suspend fun complete(
chatCompletionRequest: ChatCompletionRequest,
): ChatCompletionResponse {
return httpClient
.post("https://api.openai.com/v1/chat/completions") {
openAiHeaders()
setBody(chatCompletionRequest)
}
.body<ChatCompletionResponse>()
}

private fun HttpRequestBuilder.openAiHeaders() {
headers {
append("Content-Type", "application/json")
append("Authorization", "Bearer ${gptConfig.apiKey}")
append("OpenAI-Organization", gptConfig.organization)
}
}

@Serializable
data class OpenApiList<T>(
@SerialName("object")
val obj: String,
val data: List<T>
)

@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<Message>,
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<Content>,
)

@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<Choice>,
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("")
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading