mirror of
https://github.com/slhaf/Partner.git
synced 2026-06-27 17:49:16 +08:00
feat(impression): add structured entity evidence metadata
This commit is contained in:
@@ -7,9 +7,9 @@ import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class ActiveEntity @JvmOverloads constructor(
|
||||
timestamp: Long = System.currentTimeMillis(),
|
||||
private val _evidences: MutableList<String> = mutableListOf(),
|
||||
private val _evidences: MutableList<EntityEvidence> = mutableListOf(),
|
||||
) : BlockContent("active_entity_$timestamp", "impression") {
|
||||
val evidences: List<String>
|
||||
val evidences: List<EntityEvidence>
|
||||
get() = synchronized(_evidences) { _evidences.toList() }
|
||||
|
||||
private val _subject = AtomicReference("UNKNOWN")
|
||||
@@ -23,7 +23,15 @@ class ActiveEntity @JvmOverloads constructor(
|
||||
val projectedImpressions: Map<String, Double>
|
||||
get() = synchronized(_projectedImpressions) { _projectedImpressions.toMap() }
|
||||
|
||||
fun addEvidence(evidence: String) = synchronized(_evidences) {
|
||||
@JvmOverloads
|
||||
fun addEvidence(
|
||||
content: String,
|
||||
associationConfidence: Double = 1.0,
|
||||
source: EntityEvidence.Source = EntityEvidence.Source.USER_INPUT,
|
||||
timestamp: Long = System.currentTimeMillis(),
|
||||
) = addEvidence(EntityEvidence(content, associationConfidence, source, timestamp))
|
||||
|
||||
fun addEvidence(evidence: EntityEvidence) = synchronized(_evidences) {
|
||||
_evidences.add(evidence)
|
||||
}
|
||||
|
||||
@@ -46,7 +54,14 @@ class ActiveEntity @JvmOverloads constructor(
|
||||
"evidences",
|
||||
"evidence",
|
||||
synchronized(_evidences) { _evidences.toList() }
|
||||
)
|
||||
) { evidence ->
|
||||
setAttribute("association_confidence", evidence.associationConfidence.toString())
|
||||
setAttribute("source", evidence.source.name)
|
||||
setAttribute("timestamp", evidence.timestamp.toString())
|
||||
setAttribute("truncated", evidence.isContentTruncated().toString())
|
||||
setAttribute("original_length", evidence.content.length.toString())
|
||||
textContent = evidence.contentForContext()
|
||||
}
|
||||
|
||||
appendListElement(
|
||||
document,
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package work.slhaf.partner.core.cognition.impression
|
||||
|
||||
/**
|
||||
* Runtime evidence associated with an active entity.
|
||||
*
|
||||
* The confidence describes how strongly this evidence is associated with the
|
||||
* current active entity, not whether the evidence content itself is true.
|
||||
*/
|
||||
data class EntityEvidence @JvmOverloads constructor(
|
||||
val content: String,
|
||||
val associationConfidence: Double = 1.0,
|
||||
val source: Source = Source.USER_INPUT,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
) {
|
||||
enum class Source {
|
||||
USER_INPUT,
|
||||
ASSISTANT_REPLY
|
||||
}
|
||||
|
||||
fun isContentTruncated(maxLength: Int = CONTEXT_CONTENT_MAX_LENGTH): Boolean =
|
||||
content.length > maxLength
|
||||
|
||||
fun contentForContext(maxLength: Int = CONTEXT_CONTENT_MAX_LENGTH): String {
|
||||
if (content.length <= maxLength) {
|
||||
return content
|
||||
}
|
||||
|
||||
val available = maxLength - OMITTED_MARKER.length
|
||||
if (available <= 0) {
|
||||
return content.take(maxLength)
|
||||
}
|
||||
|
||||
val headBudget = available / 2
|
||||
val tailBudget = available - headBudget
|
||||
val headEnd = adjustHeadEnd(content, headBudget)
|
||||
val tailStart = adjustTailStart(content, content.length - tailBudget)
|
||||
|
||||
if (tailStart <= headEnd) {
|
||||
return content.take(maxLength).trimEnd()
|
||||
}
|
||||
|
||||
return content.substring(0, headEnd).trimEnd() +
|
||||
OMITTED_MARKER +
|
||||
content.substring(tailStart).trimStart()
|
||||
}
|
||||
|
||||
private fun adjustHeadEnd(source: String, preferredEnd: Int): Int {
|
||||
val safePreferredEnd = preferredEnd.coerceIn(0, source.length)
|
||||
findForwardBoundary(source, safePreferredEnd, STRONG_BOUNDARY_SEARCH_WINDOW, ::isStrongBoundary)?.let {
|
||||
return it + 1
|
||||
}
|
||||
findForwardBoundary(source, safePreferredEnd, SOFT_BOUNDARY_SEARCH_WINDOW, ::isSoftBoundary)?.let {
|
||||
return it + 1
|
||||
}
|
||||
return safePreferredEnd
|
||||
}
|
||||
|
||||
private fun adjustTailStart(source: String, preferredStart: Int): Int {
|
||||
val safePreferredStart = preferredStart.coerceIn(0, source.length)
|
||||
findBackwardBoundary(source, safePreferredStart, STRONG_BOUNDARY_SEARCH_WINDOW, ::isStrongBoundary)?.let {
|
||||
return it
|
||||
}
|
||||
findBackwardBoundary(source, safePreferredStart, SOFT_BOUNDARY_SEARCH_WINDOW, ::isSoftBoundary)?.let {
|
||||
return it
|
||||
}
|
||||
return safePreferredStart
|
||||
}
|
||||
|
||||
private fun findForwardBoundary(
|
||||
source: String,
|
||||
start: Int,
|
||||
window: Int,
|
||||
predicate: (Char) -> Boolean,
|
||||
): Int? {
|
||||
val end = (start + window).coerceAtMost(source.length)
|
||||
for (index in start until end) {
|
||||
if (predicate(source[index])) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findBackwardBoundary(
|
||||
source: String,
|
||||
start: Int,
|
||||
window: Int,
|
||||
predicate: (Char) -> Boolean,
|
||||
): Int? {
|
||||
val end = (start - window).coerceAtLeast(0)
|
||||
for (index in start downTo end + 1) {
|
||||
if (predicate(source[index - 1])) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isStrongBoundary(char: Char): Boolean = char == '\n'
|
||||
|
||||
private fun isSoftBoundary(char: Char): Boolean = when (char) {
|
||||
'。', '!', '?', ';', ';', '.' -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CONTEXT_CONTENT_MAX_LENGTH = 480
|
||||
private const val OMITTED_MARKER = "\n...[omitted]...\n"
|
||||
private const val STRONG_BOUNDARY_SEARCH_WINDOW = 120
|
||||
private const val SOFT_BOUNDARY_SEARCH_WINDOW = 80
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user