vineri, 23 mai 2025

Cleaner HTTP Request Assertions with AssertJ

I switched to using AssertJ's fluent API with .satisfies() for verifying MockWebServer requests—and I love it. It keeps the code concise and readable, and best of all, I didn’t need to define an extra variable just to run my assertions.

Original (JUnit-style):

RecordedRequest request = mockWebServer.takeRequest();
assertEquals("/graph", request.getPath());
assertTrue(request.getBody().readUtf8().contains("mutation AddSomething"));

Improved (AssertJ-style):

assertThat(mockWebServer.takeRequest()).satisfies(request -> {
    assertThat(request.getPath()).isEqualTo("/graph");
    assertThat(request.getBody().readUtf8()).contains("mutation AddSomething");
});

marți, 20 mai 2025

Extracting First Names from full_name with LLaMA 3 and Ollama in Kotlin

Assume we have a users table containing the columns full_name, last_name, and first_name. In 10% of the cases, the first_name field is NULL, but we observe that the first name is embedded within the full_name field, just without a clear delimiter (e.g., 'DUMBRAVEANURADU'). Or we can make the problem more difficult by considering compound first names, which are also not separated by spaces from the last name (e.g., 'IONESCUANDREIMIHAIL', 'POPESCOANAMARIA', 'VASILESCUIONAELENA', 'TOMAIOANDANIEL', 'MOLDOVANMARIAGABRIELA').

I would like to tell my experience of solving such a problem.

1. Export all rows where first_name is NULL into a data.csv file (PgAdmin > Save results to file).

2. I started Ollama and loaded the LLaMA 3 model locally

docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama
docker exec -it 57b53e2bc095 ollama pull llama3 
3. I Created a Kotlin Project and Asked ChatGPT for Code Fragments
build.gradle.kts
dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0")
    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
    testImplementation(kotlin("test"))
}

4. I Asked ChatGPT How to Construct the Prompt
Main.kt
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.io.File
import java.io.PrintWriter
import java.util.concurrent.TimeUnit

fun main() {
    val client = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)  // Time to establish connection
        .readTimeout(30, TimeUnit.SECONDS)     // Time to wait for server response
        .writeTimeout(15, TimeUnit.SECONDS)    // Time to send request data
        .build()

    val mapper = jacksonObjectMapper()

    val inputStream = object {}.javaClass.getResourceAsStream("/data-1746619055858.csv")
        ?: error("File not found in resources")

    val writer = PrintWriter(File("output.csv"))

    writer.println(listOf("id", "full_name", "last_name", "first_name", "sql")
        .joinToString(",") { "\"$it\"" })

    inputStream.bufferedReader().useLines { lines ->
        lines
            .drop(1) // Skip the header line
            .take(100)
            .forEach {
                val columns = it.split(",")
                    .map { it.trim('"') } // Remove surrounding quotes

                val id = columns[0]
                val fullName = columns[1]
                
                println("ID: $id, Name: $fullName")

                val json = mapper.writeValueAsString(
                    mapOf(
                        "model" to "llama3",
//                        "system" to "The following string contains a full name in uppercase without clear delimiters between the last name(s) and the first name(s).\nPlease analyze the name and split it into two parts:\n\nlastName: which can include compound surnames.\n\nfirstName: whichANDREI-MIHAIL may also be a compound name.\nTry to detect where the first name begins even if it is attached directly to the last name (e.g., DUMBRAVEANURADU → DUMBRAVEANU + RADU).\nReturn the result in JSON format like:\n{ \"lastName\": \"...\", \"firstName\": \"...\" }",
                        "system" to """
                            The following string contains a full name in uppercase without clear delimiters between the last name(s) and the first name(s).
                            Your task is to analyze and split it into two parts:
                            - "lastName: which can include compound surnames"
                            - "firstName: which may also be a compound name
                            Try to detect where the first name begins even if it is attached directly to the last name (e.g., DUMBRAVEANURADU → DUMBRAVEANU + RADU).     
                            Important: Return only a JSON object with the following format:
                            { "lastName": "...", "firstName": "..." }
                            Do not explain or add any other text. Only return valid JSON.
                           
                            Examples:
                            - "DUMBRAVEANURADU" should be split into "lastName": "DUMBRAVEANU", "firstName": "RADU"
                            - "IONESCUANDREIMIHAIL" should be split into "lastName": "IONESCU", "firstName": "ANDREI-MIHAIL"
                            - "POPESCOANAMARIA" should be split into "lastName": "POPESCO", "firstName": "ANA-MARIA"                                                       
                        """.trimIndent(),
//                        "prompt" to participantName,
                        "prompt" to "Name: $participantName\nReturn JSON only.",
                        "stream" to false
                    )
                )
                

                val request = Request.Builder()
                    .url("http://localhost:11434/api/generate")
                    .post(json.toRequestBody("application/json".toMediaType()))
                    .build()

                client.newCall(request).execute().use { response ->
                    if (!response.isSuccessful) {
                        println("Unexpected response: $response")
                    } else {
                        val body = response.body?.string()
                        val parsed = mapper.readValue<Map<String, Any>>(body!!)
                        println(parsed["response"])
                        val response =  mapper.readValue<Map<String, Any>>(parsed["response"].toString())
                        val lastName = response?.get("lastName")
                        val firstName = response?.get("firstName")

                        val sql = """
                            UPDATE users
                            SET last_name = '$lastName',
                                first_name = '$firstName'
                            WHERE id = '$id' AND full_name = '$fullName';
                        """.trimIndent().replace("\n", " ")
                        writer.println(listOf(caseNumber, participantName, response?.get("lastName"), response?.get("firstName"), sql)
                            .joinToString(",") { "\"$it\"" })
                    }
                }
            }
    }

    writer.close()
}

5. Execute and Analyze the Output File

vineri, 16 mai 2025

Generating Structured Output with OpenAI Java SDK

When working with OpenAI's API responses in Java, it's often useful to request structured output instead of plain text. This enables better parsing, validation, and downstream processing. The OpenAI Java SDK supports this through JSON Schema-based formats.

Reference: StructuredOutputsExample.java (GitHub) 

https://github.com/openai/openai-java/blob/main/openai-java-example/src/main/java/com/openai/example/StructuredOutputsExample.java

            ResponseFormatTextJsonSchemaConfig.Schema schema = ResponseFormatTextJsonSchemaConfig.Schema.builder()
                    .putAdditionalProperty("type", JsonValue.from("object"))
                    .putAdditionalProperty(
                        "properties", JsonValue.from(Map.of("employees", Map.of("type", "array", "items", Map.of("type", "string")))))
                    .putAdditionalProperty("required", JsonValue.from(List.of("employees")))
                    .putAdditionalProperty("additionalProperties", JsonValue.from(false))
                    .build();
            createParams = createParams.toBuilder()
//                .instructions(assistant.instructions())
                .text(ResponseTextConfig.builder()
                    .format(ResponseFormatTextJsonSchemaConfig.builder()
                        .name("employee-list")
                        .schema(schema)
                        .build())
                    .build())
                .build();
                
        openAiClient.getClient().responses().create(createParams).output().stream()
                .flatMap(item -> item.message().stream())
                .flatMap(message -> message.content().stream())
                .flatMap(content -> content.outputText().stream())
                .forEach(outputText -> System.err.println(outputText.text()));       

Convert Assistant defined response format. 

Assistant assistant = openAiClient.getClient().beta()
            .assistants()
            .retrieve(AssistantRetrieveParams.builder()
            .assistantId(openAiAssistantId)
                .build());
ResponseFormatJsonSchema.JsonSchema jsonSchema = assistant.responseFormat().get()
                .asResponseFormatJsonSchema()
                .jsonSchema();
                
                
ResponseFormatTextJsonSchemaConfig.Schema schema = ResponseFormatTextJsonSchemaConfig.Schema.builder()
                .additionalProperties(jsonSchema.schema().get()._additionalProperties())
                .build();
        ResponseCreateParams createParams = ResponseCreateParams.builder()
            .input(input)
            .model(model)
            .text(ResponseTextConfig.builder()
                .format(ResponseFormatTextJsonSchemaConfig.builder()
                    .name(jsonSchema.name())
                    .schema(schema)
                    .build())
                .build())
            .build();                

luni, 5 mai 2025

Configure svenstaro/keycloak-http-webhook-provider for Keycloak 24.0.5

To configure svenstaro/keycloak-http-webhook-provider with Keycloak 24.0.5, I followed these steps:

Clone the repository: https://github.com/svenstaro/keycloak-http-webhook-provider

Create a Dockerfile in the root of the repository with the following content to build a custom Keycloak image that includes the webhook provider:

FROM quay.io/keycloak/keycloak:24.0.5-0

COPY target/keycloak_http_webhook_provider.jar /opt/keycloak/providers/

ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start-dev"]

Build the JAR

Build the Docker image:

docker buildx build --debug -t keycloak-http-webhook-provider .

Run the container with the required environment variables to configure the webhook:

docker run -e KC_SPI_EVENTS_LISTENER_HTTP_WEBHOOK_SERVER_URL=https://your-webhook-url \
           -e KC_SPI_EVENTS_LISTENER_HTTP_WEBHOOK_USERNAME=youruser \
           -e KC_SPI_EVENTS_LISTENER_HTTP_WEBHOOK_PASSWORD=yourpassword \
           -e KEYCLOAK_ADMIN=admin \
           -e KEYCLOAK_ADMIN_PASSWORD=admin \
           -p 8080:8080 keycloak-http-webhook-provider