Skip to content

Commit 3501484

Browse files
committed
Make typed find methods return interfaces
1 parent 7f84388 commit 3501484

2 files changed

Lines changed: 48 additions & 8 deletions

File tree

embabel-agent-rag/embabel-agent-rag-core/src/main/kotlin/com/embabel/agent/rag/model/NamedEntityData.kt

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.embabel.agent.core.DomainType
1919
import com.embabel.agent.core.JvmType
2020
import com.embabel.common.util.indent
2121
import com.fasterxml.jackson.databind.ObjectMapper
22+
import org.slf4j.LoggerFactory
2223
import java.lang.reflect.InvocationHandler
2324
import java.lang.reflect.Method
2425
import java.lang.reflect.Proxy
@@ -78,21 +79,34 @@ interface NamedEntityData : EntityData, NamedEntity {
7879
* This allows hydration even when [linkedDomainType] is not set,
7980
* as long as the entity's labels match the target type and the properties are compatible.
8081
*
81-
* @param objectMapper the ObjectMapper to use for deserialization
82+
* For interface types, a dynamic proxy is created.
83+
* For concrete classes, Jackson deserialization is used.
84+
*
85+
* @param objectMapper the ObjectMapper to use for deserialization (only for concrete classes)
8286
* @param type the target class to hydrate to
8387
* @return the hydrated instance, or null if hydration fails
8488
*/
89+
@Suppress("UNCHECKED_CAST")
8590
fun <T : NamedEntity> toTypedInstance(objectMapper: ObjectMapper, type: Class<T>): T? {
8691
return try {
87-
// Build the full property map including id, name, description
88-
val allProperties = buildMap {
89-
put("id", id)
90-
put("name", name)
91-
put("description", description)
92-
putAll(properties)
92+
if (type.isInterface) {
93+
// Use dynamic proxy for interfaces
94+
toInstance(type) as T
95+
} else {
96+
// Use Jackson for concrete classes
97+
val allProperties = buildMap {
98+
put("id", id)
99+
put("name", name)
100+
put("description", description)
101+
putAll(properties)
102+
}
103+
objectMapper.convertValue(allProperties, type)
93104
}
94-
objectMapper.convertValue(allProperties, type)
95105
} catch (e: Exception) {
106+
LoggerFactory.getLogger(type).warn(
107+
"Failed to hydrate NamedEntityData (id=$id) to type ${type.name}: ${e.message}",
108+
e
109+
)
96110
null
97111
}
98112
}
@@ -179,6 +193,7 @@ internal class NamedEntityInvocationHandler(
179193
val indent = args?.getOrNull(1) as? Int ?: 0
180194
entityData.infoString(verbose, indent)
181195
}
196+
182197
"propertiesToPersist" -> entityData.propertiesToPersist()
183198

184199
// Kotlin property getter pattern: getXxx() -> property "xxx"

embabel-agent-rag/embabel-agent-rag-core/src/test/kotlin/com/embabel/agent/rag/model/NamedEntityProxyTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package com.embabel.agent.rag.model
1717

18+
import com.fasterxml.jackson.databind.ObjectMapper
19+
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
1820
import org.junit.jupiter.api.Assertions.*
1921
import org.junit.jupiter.api.Test
2022

@@ -246,4 +248,27 @@ class NamedEntityProxyTest {
246248
// Should contain the property
247249
assertTrue(embeddable.contains("department=Dev"))
248250
}
251+
252+
@Test
253+
fun `toTypedInstance works with interface type`() {
254+
val objectMapper = ObjectMapper().registerKotlinModule()
255+
val entityData = SimpleNamedEntityData(
256+
id = "place-1",
257+
name = "Blue Note",
258+
description = "Jazz club",
259+
labels = setOf("MusicPlace"),
260+
properties = mapOf("location" to "New York")
261+
)
262+
263+
val result: MusicPlace? = entityData.toTypedInstance(objectMapper, MusicPlace::class.java)
264+
265+
assertNotNull(result)
266+
assertEquals("place-1", result!!.id)
267+
assertEquals("Blue Note", result.name)
268+
assertEquals("New York", result.location)
269+
}
270+
}
271+
272+
interface MusicPlace : NamedEntity {
273+
val location: String
249274
}

0 commit comments

Comments
 (0)