mirror of
https://github.com/slhaf/Partner.git
synced 2026-06-27 17:49:16 +08:00
feat(impression): introduce active entity recall model
This commit is contained in:
@@ -3,18 +3,30 @@ package work.slhaf.partner.core.cognition.impression
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import work.slhaf.partner.core.cognition.context.BlockContent
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class ActiveEntity @JvmOverloads constructor(
|
||||
timestamp: Long = System.currentTimeMillis(),
|
||||
val runtimeId: String = newActiveEntityRuntimeId(),
|
||||
val createdAt: Instant = Instant.now(),
|
||||
boundEntityUuid: String? = null,
|
||||
private val _evidences: MutableList<EntityEvidence> = mutableListOf(),
|
||||
) : BlockContent("active_entity_$timestamp", "impression") {
|
||||
) : BlockContent("active_entity_$runtimeId", "impression") {
|
||||
val evidences: List<EntityEvidence>
|
||||
get() = synchronized(_evidences) { _evidences.toList() }
|
||||
|
||||
@Volatile
|
||||
var lastMentionedAt: Instant = createdAt
|
||||
private set
|
||||
|
||||
private val _subject = AtomicReference("UNKNOWN")
|
||||
val subject: String get() = _subject.get()
|
||||
|
||||
private val _boundEntityUuid = AtomicReference<String?>(boundEntityUuid)
|
||||
val boundEntityUuid: String? get() = _boundEntityUuid.get()
|
||||
|
||||
private val _projectedFeatures: MutableMap<String, Double> = mutableMapOf()
|
||||
val projectedFeatures: Map<String, Double>
|
||||
get() = synchronized(_projectedFeatures) { _projectedFeatures.toMap() }
|
||||
@@ -33,10 +45,17 @@ class ActiveEntity @JvmOverloads constructor(
|
||||
|
||||
fun addEvidence(evidence: EntityEvidence) = synchronized(_evidences) {
|
||||
_evidences.add(evidence)
|
||||
touch(Instant.ofEpochMilli(evidence.timestamp))
|
||||
}
|
||||
|
||||
fun updateSubject(subject: String) = _subject.set(subject)
|
||||
|
||||
fun bindEntity(uuid: String?) = _boundEntityUuid.set(uuid)
|
||||
|
||||
fun touch(time: Instant = Instant.now()) {
|
||||
lastMentionedAt = time
|
||||
}
|
||||
|
||||
fun addProjectedFeatures(vararg features: Pair<String, Double>) = synchronized(_projectedFeatures) {
|
||||
features.forEach { _projectedFeatures[it.first] = it.second }
|
||||
}
|
||||
@@ -46,6 +65,11 @@ class ActiveEntity @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun fillXml(document: Document, root: Element) {
|
||||
root.setAttribute("runtime_id", runtimeId)
|
||||
boundEntityUuid?.let { root.setAttribute("bound_entity_uuid", it) }
|
||||
root.setAttribute("created_at", modelTime(createdAt))
|
||||
root.setAttribute("last_mentioned_at", modelTime(lastMentionedAt))
|
||||
|
||||
appendTextElement(document, root, "subject", subject)
|
||||
|
||||
appendListElement(
|
||||
@@ -85,4 +109,10 @@ class ActiveEntity @JvmOverloads constructor(
|
||||
textContent = entry.key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun modelTime(time: Instant): String =
|
||||
time.atZone(ZoneId.systemDefault()).toString()
|
||||
}
|
||||
|
||||
private fun newActiveEntityRuntimeId(): String =
|
||||
UUID.randomUUID().toString().replace("-", "").take(12)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
data class EntityAssociationMatch(
|
||||
val target: ImpressionSearchTarget,
|
||||
val score: Double,
|
||||
val hits: List<ImpressionSearchHit> = emptyList(),
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
data class ImpressionSearchDocument(
|
||||
val id: String,
|
||||
val target: ImpressionSearchTarget,
|
||||
val field: ImpressionSearchField,
|
||||
val text: String,
|
||||
val weight: Double = 1.0,
|
||||
val metadata: Map<String, String> = emptyMap(),
|
||||
)
|
||||
@@ -0,0 +1,138 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
import work.slhaf.partner.core.cognition.impression.ActiveEntity
|
||||
import work.slhaf.partner.core.cognition.impression.Entity
|
||||
|
||||
object ImpressionSearchDocuments {
|
||||
|
||||
fun fromActiveEntity(activeEntity: ActiveEntity): List<ImpressionSearchDocument> {
|
||||
val target = ImpressionSearchTarget(
|
||||
ImpressionSearchTarget.Type.ACTIVE_ENTITY,
|
||||
activeEntity.runtimeId
|
||||
)
|
||||
val metadata = activeEntity.boundEntityUuid
|
||||
?.let { mapOf("boundEntityUuid" to it) }
|
||||
.orEmpty()
|
||||
|
||||
return buildList {
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "active:${activeEntity.runtimeId}:subject",
|
||||
target = target,
|
||||
field = ImpressionSearchField.SUBJECT,
|
||||
text = activeEntity.subject,
|
||||
weight = SUBJECT_WEIGHT,
|
||||
metadata = metadata,
|
||||
)
|
||||
)
|
||||
|
||||
activeEntity.evidences.forEachIndexed { index, evidence ->
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "active:${activeEntity.runtimeId}:evidence:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.EVIDENCE,
|
||||
text = evidence.contentForContext(),
|
||||
weight = EVIDENCE_WEIGHT * evidence.associationConfidence,
|
||||
metadata = metadata,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
activeEntity.projectedFeatures.entries.forEachIndexed { index, entry ->
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "active:${activeEntity.runtimeId}:feature:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.FEATURE,
|
||||
text = entry.key,
|
||||
weight = FEATURE_WEIGHT * entry.value,
|
||||
metadata = metadata,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
activeEntity.projectedImpressions.entries.forEachIndexed { index, entry ->
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "active:${activeEntity.runtimeId}:impression:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.IMPRESSION,
|
||||
text = entry.key,
|
||||
weight = IMPRESSION_WEIGHT * entry.value,
|
||||
metadata = metadata,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fromEntity(entity: Entity): List<ImpressionSearchDocument> {
|
||||
val target = ImpressionSearchTarget(
|
||||
ImpressionSearchTarget.Type.ENTITY,
|
||||
entity.uuid
|
||||
)
|
||||
|
||||
return buildList {
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "entity:${entity.uuid}:subject",
|
||||
target = target,
|
||||
field = ImpressionSearchField.SUBJECT,
|
||||
text = entity.subject,
|
||||
weight = SUBJECT_WEIGHT,
|
||||
)
|
||||
)
|
||||
|
||||
entity.snapshotFeatures().keys.forEachIndexed { index, feature ->
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "entity:${entity.uuid}:feature:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.FEATURE,
|
||||
text = feature,
|
||||
weight = FEATURE_WEIGHT,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
entity.snapshotImpressions().keys.forEachIndexed { index, impression ->
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "entity:${entity.uuid}:impression:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.IMPRESSION,
|
||||
text = impression,
|
||||
weight = IMPRESSION_WEIGHT,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
entity.showRelations().forEachIndexed { index, relation ->
|
||||
val relationText = buildString {
|
||||
append(relation.target)
|
||||
relation.relations.keys.forEach { name ->
|
||||
append(' ')
|
||||
append(name)
|
||||
}
|
||||
}
|
||||
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "entity:${entity.uuid}:relation:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.RELATION,
|
||||
text = relationText,
|
||||
weight = RELATION_WEIGHT,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val SUBJECT_WEIGHT = 1.0
|
||||
private const val FEATURE_WEIGHT = 0.85
|
||||
private const val IMPRESSION_WEIGHT = 0.75
|
||||
private const val RELATION_WEIGHT = 0.65
|
||||
private const val EVIDENCE_WEIGHT = 0.8
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
enum class ImpressionSearchField {
|
||||
SUBJECT,
|
||||
FEATURE,
|
||||
IMPRESSION,
|
||||
RELATION,
|
||||
EVIDENCE
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
data class ImpressionSearchHit(
|
||||
val document: ImpressionSearchDocument,
|
||||
val score: Double,
|
||||
val matchedTerms: Set<String> = emptySet(),
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
data class ImpressionSearchTarget(
|
||||
val type: Type,
|
||||
val id: String,
|
||||
) {
|
||||
enum class Type {
|
||||
ACTIVE_ENTITY,
|
||||
ENTITY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
interface ImpressionTextSearch {
|
||||
|
||||
fun rebuild(documents: Collection<ImpressionSearchDocument>)
|
||||
|
||||
fun upsert(document: ImpressionSearchDocument)
|
||||
|
||||
fun removeByTarget(target: ImpressionSearchTarget)
|
||||
|
||||
fun search(
|
||||
query: String,
|
||||
limit: Int = 20,
|
||||
): List<ImpressionSearchHit>
|
||||
}
|
||||
Reference in New Issue
Block a user