CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-com-embabel-agent--embabel-agent-common

Common AI framework utilities for the Embabel Agent system including LLM configuration, output converters, prompt contributors, and embedding service abstractions.

Overview
Eval results
Files

output-converters.mddocs/core/

Output Converters

Jackson-based converters for transforming LLM text responses into typed objects with automatic JSON Schema generation and lenient parsing.

Core Concepts

JacksonOutputConverter: Base converter with schema generation FilteringJacksonOutputConverter: Converter with property filtering Lenient Parsing: Handles malformed JSON common in LLM responses

Basic Usage

open class JacksonOutputConverter<T>(
    clazz: Class<T>,
    objectMapper: ObjectMapper
) : StructuredOutputConverter<T> {
    val objectMapper: ObjectMapper
    val jsonSchema: String
    fun convert(text: String): T?
    fun getFormat(): String
    protected open fun postProcessSchema(jsonNode: JsonNode)
}

Simple Example

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule

data class Person(val name: String, val age: Int, val email: String)

val mapper = ObjectMapper().registerKotlinModule()
val converter = JacksonOutputConverter(Person::class.java, mapper)

// Convert LLM response
val response = """{"name": "Alice", "age": 30, "email": "alice@example.com"}"""
val person = converter.convert(response)
// person = Person(name=Alice, age=30, email=alice@example.com)

Check for Errors

val person = converter.convert(llmResponse)
if (person == null) {
    // Parsing failed
    logger.error("Failed to parse LLM response")
}

JSON Schema

Automatically generate schema from types for LLM prompts.

Get Schema

val schema = converter.jsonSchema
// Returns: {"type":"object","properties":{"name":{"type":"string"},...}}

Include in Prompt

val prompt = """
    Extract person information from text.

    ${converter.getFormat()}

    Text: "John Doe is 35 years old, email: john@example.com"
""".trimIndent()

Schema Features

Respects Jackson annotations:

data class Person(
    @JsonProperty("full_name")
    @JsonPropertyDescription("Person's full name")
    val name: String,

    @JsonIgnore
    val internalId: String
)
// Schema will use "full_name" and exclude "internalId"

Supports complex types:

data class User(
    val name: String,
    val addresses: List<Address>,
    val metadata: Map<String, Any>
)
// Schema includes nested object definitions

Lenient Parsing

Handles common LLM output issues automatically.

Supported Issues

Markdown code blocks:

val response = """
    ```json
    {"name": "Bob", "age": 25}
    ```
"""
converter.convert(response) // Works!

Trailing commas:

val response = """{"name": "Alice", "age": 30,}"""
converter.convert(response) // Works!

Unquoted field names:

val response = """{name: "Charlie", age: 28}"""
converter.convert(response) // Works!

Single quotes:

val response = """{'name': 'David', 'age': 35}"""
converter.convert(response) // Works!

Comments:

val response = """
{
    "name": "Eve", // user name
    "age": 22 /* years old */
}
"""
converter.convert(response) // Works!

Malformed escaped quotes:

val response = """{"name": "\"Frank\""}"""
converter.convert(response) // Works!

How It Works

The converter attempts multiple parsing strategies:

  1. Standard JSON parsing
  2. Markdown extraction + JSON parsing
  3. Lenient JSON parsing (fixes common issues)
  4. Returns null if all strategies fail

Property Filtering

Exclude properties from JSON schema while keeping them in the target type.

open class FilteringJacksonOutputConverter<T>(
    clazz: Class<T>,
    objectMapper: ObjectMapper,
    propertyFilter: Predicate<String>
) : JacksonOutputConverter<T>

Basic Filtering

data class User(
    val id: String,
    val name: String,
    val email: String,
    val internalId: String,
    val systemMetadata: String
)

// Filter out internal fields
val filter = Predicate<String> { propertyName ->
    !propertyName.startsWith("internal") &&
    !propertyName.startsWith("system")
}

val converter = FilteringJacksonOutputConverter(
    User::class.java,
    mapper,
    filter
)

// Schema only includes: id, name, email
val schema = converter.jsonSchema

Filter Patterns

Exclude by prefix:

val filter = Predicate<String> { !it.startsWith("_") }

Whitelist only:

val whitelist = setOf("name", "email", "age")
val filter = Predicate<String> { it in whitelist }

Exclude by suffix:

val filter = Predicate<String> { !it.endsWith("Internal") }

Complex logic:

val filter = Predicate<String> { property ->
    !property.startsWith("_") &&
    !property.endsWith("Internal") &&
    property != "systemData"
}

Use Cases

Hide sensitive fields from LLM but keep in code:

data class User(
    val name: String,
    val email: String,
    val password: String // Not in schema
)
val filter = Predicate<String> { it != "password" }

Separate input/output fields:

data class Task(
    val input: String,
    val expectedOutput: String, // Not in schema for LLM
    val actualOutput: String?
)
val filter = Predicate<String> { it != "expectedOutput" }

Generic Types

Use ParameterizedTypeReference for generic types.

open class JacksonOutputConverter<T>(
    typeReference: ParameterizedTypeReference<T>,
    objectMapper: ObjectMapper
) : StructuredOutputConverter<T>

Example

import org.springframework.core.ParameterizedTypeReference

data class Result<T>(val data: T, val status: String)

val typeRef = object : ParameterizedTypeReference<Result<Person>>() {}
val converter = JacksonOutputConverter(typeRef, mapper)

val response = """{"data": {"name": "Alice", "age": 30}, "status": "ok"}"""
val result = converter.convert(response)
// result = Result(data=Person(name=Alice, age=30), status=ok)

List types:

val typeRef = object : ParameterizedTypeReference<List<Person>>() {}
val converter = JacksonOutputConverter(typeRef, mapper)

Map types:

val typeRef = object : ParameterizedTypeReference<Map<String, Person>>() {}
val converter = JacksonOutputConverter(typeRef, mapper)

Custom Schema Processing

Override postProcessSchema() to modify generated schema.

class CustomConverter<T>(
    clazz: Class<T>,
    objectMapper: ObjectMapper
) : JacksonOutputConverter<T>(clazz, objectMapper) {
    override fun postProcessSchema(jsonNode: JsonNode) {
        if (jsonNode is ObjectNode) {
            // Disallow additional properties
            jsonNode.put("additionalProperties", false)

            // Add custom fields
            jsonNode.put("$schema", "http://json-schema.org/draft-07/schema#")

            // Modify existing properties
            val properties = jsonNode.get("properties") as? ObjectNode
            properties?.fields()?.forEach { (name, prop) ->
                if (prop is ObjectNode) {
                    prop.put("description", "Field: $name")
                }
            }
        }
    }
}

Common Modifications

Add schema version:

jsonNode.put("$schema", "http://json-schema.org/draft-07/schema#")

Disallow additional properties:

jsonNode.put("additionalProperties", false)

Add required fields:

val required = jsonNode.putArray("required")
required.add("name")
required.add("email")

Add examples:

jsonNode.putArray("examples").add(
    mapper.createObjectNode().apply {
        put("name", "Alice")
        put("age", 30)
    }
)

Integration Patterns

With LLM Client

fun extractPerson(text: String, llmClient: LLMClient): Person? {
    val converter = JacksonOutputConverter(Person::class.java, mapper)

    val prompt = """
        Extract person information as JSON: ${converter.jsonSchema}
        Text: $text
    """.trimIndent()

    val response = llmClient.call(prompt)
    return converter.convert(response)
}

With Validation

fun convertWithValidation(text: String): Person? {
    val person = converter.convert(text)

    return person?.takeIf { it.age in 0..150 && it.email.contains("@") }
}

Batch Conversion

fun convertBatch(responses: List<String>): List<Person> {
    return responses.mapNotNull { converter.convert(it) }
}

Common Issues

Null Results

Cause: Malformed JSON that couldn't be parsed Solution: Check LLM response format, adjust temperature, improve prompt

Missing Fields

Cause: LLM didn't include all required fields Solution: Make fields nullable or provide defaults

data class Person(
    val name: String = "Unknown",
    val age: Int? = null
)

Wrong Types

Cause: LLM used wrong type (e.g., string instead of number) Solution: Use lenient parsing or add type coercion

// Accept both string and int for age
data class Person(
    val name: String,
    val age: Any // Convert later
)

→ See Error Handling for detailed troubleshooting → See Performance for optimization tips

tessl i tessl/maven-com-embabel-agent--embabel-agent-common@0.3.1

docs

index.md

quick-reference.md

README.md

tile.json