mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 08:43:02 +08:00
refactor(context): make block activation/rendering exposure-aware and use rendered projections in aggregation
This commit is contained in:
@@ -51,7 +51,12 @@ class ContextWorkspace {
|
||||
continue
|
||||
}
|
||||
|
||||
val activationScore = block.activate()
|
||||
val exposure = if (primaryDomain in matchedDomains) {
|
||||
ContextBlock.Exposure.PRIMARY
|
||||
} else {
|
||||
ContextBlock.Exposure.SECONDARY
|
||||
}
|
||||
val activationScore = block.activate(exposure)
|
||||
if (activationScore <= 0.0) {
|
||||
iterator.remove()
|
||||
continue
|
||||
@@ -61,7 +66,7 @@ class ContextWorkspace {
|
||||
block = block,
|
||||
domainWeight = matchedDomains.sumOf { domainWeights.getValue(it) },
|
||||
activationScore = activationScore,
|
||||
forceFullRender = primaryDomain in matchedDomains
|
||||
renderedBlock = block.render(exposure)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,11 +91,7 @@ class ContextWorkspace {
|
||||
}
|
||||
|
||||
private fun renderResolvedBlock(resolved: ResolvedContextBlock): BlockContent {
|
||||
return if (resolved.forceFullRender) {
|
||||
resolved.block.blockContent
|
||||
} else {
|
||||
resolved.block.render()
|
||||
}
|
||||
return resolved.renderedBlock
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +129,7 @@ private data class ResolvedContextBlock(
|
||||
val block: ContextBlock,
|
||||
val domainWeight: Int,
|
||||
val activationScore: Double,
|
||||
val forceFullRender: Boolean
|
||||
val renderedBlock: BlockContent
|
||||
)
|
||||
|
||||
data class ContextBlock @JvmOverloads constructor(
|
||||
@@ -155,6 +156,16 @@ data class ContextBlock @JvmOverloads constructor(
|
||||
*/
|
||||
private val activateFactor: Double
|
||||
) {
|
||||
internal enum class Exposure {
|
||||
PRIMARY,
|
||||
SECONDARY
|
||||
}
|
||||
|
||||
private enum class ProjectionLevel {
|
||||
ABSTRACT,
|
||||
COMPACT,
|
||||
FULL
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认活跃分数,降低至0时将在 [ContextWorkspace] 中移除该 block
|
||||
@@ -185,9 +196,23 @@ data class ContextBlock @JvmOverloads constructor(
|
||||
return activationScore
|
||||
}
|
||||
|
||||
fun activate(): Double {
|
||||
internal fun activate(exposure: Exposure): Double {
|
||||
refreshByElapsedTime()
|
||||
activationScore = min(100.0, activationScore + activateFactor)
|
||||
val currentLevel = currentProjectionLevel()
|
||||
val increasedScore = when (exposure) {
|
||||
Exposure.PRIMARY -> activationScore + when (currentLevel) {
|
||||
ProjectionLevel.FULL -> activateFactor
|
||||
ProjectionLevel.COMPACT -> activateFactor * 0.6
|
||||
ProjectionLevel.ABSTRACT -> activateFactor * 0.6
|
||||
}
|
||||
|
||||
Exposure.SECONDARY -> activationScore + when (currentLevel) {
|
||||
ProjectionLevel.COMPACT -> activateFactor * 0.2
|
||||
ProjectionLevel.ABSTRACT -> activateFactor * 0.1
|
||||
ProjectionLevel.FULL -> 0.0
|
||||
}
|
||||
}
|
||||
activationScore = min(activationCeiling(exposure, currentLevel), increasedScore)
|
||||
return activationScore
|
||||
}
|
||||
|
||||
@@ -202,11 +227,48 @@ data class ContextBlock @JvmOverloads constructor(
|
||||
return this.sourceKey == contextBlock.sourceKey
|
||||
}
|
||||
|
||||
internal fun render(exposure: Exposure): BlockContent {
|
||||
return when (exposure) {
|
||||
Exposure.PRIMARY -> when (currentProjectionLevel()) {
|
||||
ProjectionLevel.FULL -> blockContent
|
||||
ProjectionLevel.COMPACT, ProjectionLevel.ABSTRACT -> compactBlock
|
||||
}
|
||||
|
||||
Exposure.SECONDARY -> when (currentProjectionLevel()) {
|
||||
ProjectionLevel.ABSTRACT -> abstractBlock
|
||||
ProjectionLevel.COMPACT, ProjectionLevel.FULL -> compactBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun render(): BlockContent {
|
||||
return when (currentProjectionLevel()) {
|
||||
ProjectionLevel.ABSTRACT -> abstractBlock
|
||||
ProjectionLevel.COMPACT -> compactBlock
|
||||
ProjectionLevel.FULL -> blockContent
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentProjectionLevel(): ProjectionLevel {
|
||||
return when {
|
||||
activationScore < 30 -> abstractBlock
|
||||
activationScore < 70 -> compactBlock
|
||||
else -> blockContent
|
||||
activationScore < ABSTRACT_TO_COMPACT_THRESHOLD -> ProjectionLevel.ABSTRACT
|
||||
activationScore < COMPACT_TO_FULL_THRESHOLD -> ProjectionLevel.COMPACT
|
||||
else -> ProjectionLevel.FULL
|
||||
}
|
||||
}
|
||||
|
||||
private fun activationCeiling(exposure: Exposure, currentLevel: ProjectionLevel): Double {
|
||||
return when (exposure) {
|
||||
Exposure.PRIMARY -> when (currentLevel) {
|
||||
ProjectionLevel.ABSTRACT -> COMPACT_TO_FULL_THRESHOLD - PROJECTION_EPSILON
|
||||
ProjectionLevel.COMPACT, ProjectionLevel.FULL -> MAX_ACTIVATION_SCORE
|
||||
}
|
||||
|
||||
Exposure.SECONDARY -> when (currentLevel) {
|
||||
ProjectionLevel.ABSTRACT -> ABSTRACT_TO_COMPACT_THRESHOLD - PROJECTION_EPSILON
|
||||
ProjectionLevel.COMPACT -> COMPACT_TO_FULL_THRESHOLD - PROJECTION_EPSILON
|
||||
ProjectionLevel.FULL -> MAX_ACTIVATION_SCORE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +276,13 @@ data class ContextBlock @JvmOverloads constructor(
|
||||
val blockName: String,
|
||||
val source: String
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val MAX_ACTIVATION_SCORE = 100.0
|
||||
private const val ABSTRACT_TO_COMPACT_THRESHOLD = 30.0
|
||||
private const val COMPACT_TO_FULL_THRESHOLD = 70.0
|
||||
private const val PROJECTION_EPSILON = 0.000001
|
||||
}
|
||||
}
|
||||
|
||||
private class AggregatedBlockContent(
|
||||
@@ -221,11 +290,7 @@ private class AggregatedBlockContent(
|
||||
) : BlockContent(
|
||||
groupedBlocks.first().block.sourceKey.blockName,
|
||||
groupedBlocks.first().block.sourceKey.source,
|
||||
groupedBlocks.maxByOrNull {
|
||||
if (it.forceFullRender) it.block.blockContent.urgency.ordinal else it.block.render().urgency.ordinal
|
||||
}?.let {
|
||||
if (it.forceFullRender) it.block.blockContent.urgency else it.block.render().urgency
|
||||
} ?: Urgency.NORMAL
|
||||
groupedBlocks.maxByOrNull { it.renderedBlock.urgency.ordinal }?.renderedBlock?.urgency ?: Urgency.NORMAL
|
||||
) {
|
||||
|
||||
override fun fillXml(document: Document, root: Element) {
|
||||
@@ -238,11 +303,7 @@ private class AggregatedBlockContent(
|
||||
groupedBlocks.forEachIndexed { index, groupedBlock ->
|
||||
val tagName = if (index == snapshotIndex) "snapshot" else "history_snapshot"
|
||||
val wrapper = document.createElement(tagName)
|
||||
val renderedBlock = if (groupedBlock.forceFullRender) {
|
||||
groupedBlock.block.blockContent
|
||||
} else {
|
||||
groupedBlock.block.render()
|
||||
}
|
||||
val renderedBlock = groupedBlock.renderedBlock
|
||||
wrapper.setAttribute("source", renderedBlock.source)
|
||||
wrapper.setAttribute("urgency", renderedBlock.urgency.name.lowercase(Locale.ROOT))
|
||||
root.appendChild(wrapper)
|
||||
|
||||
@@ -64,13 +64,13 @@ class ContextWorkspaceTest {
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf("low", "mid", "high"),
|
||||
resolved.map { (it as TestBlockContent).content }
|
||||
listOf("low-compact", "mid-compact", "high"),
|
||||
resolved.blocks.map { (it as TestBlockContent).content }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve uses activation score only within same source`() {
|
||||
fun `resolve aggregates same source blocks while preserving activation ordering within the group`() {
|
||||
val manager = ContextWorkspace()
|
||||
val older = contextBlock(
|
||||
blockName = "memory",
|
||||
@@ -99,10 +99,14 @@ class ContextWorkspaceTest {
|
||||
|
||||
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
|
||||
|
||||
assertEquals(
|
||||
listOf("older", "newer", "other-source"),
|
||||
resolved.map { (it as TestBlockContent).content }
|
||||
)
|
||||
assertEquals(2, resolved.blocks.size)
|
||||
assertEquals("other-source", (resolved.blocks[1] as TestBlockContent).content)
|
||||
|
||||
val aggregatedXml = resolved.blocks[0].encodeToXmlString()
|
||||
assertTrue(aggregatedXml.contains("<snapshot"))
|
||||
assertTrue(aggregatedXml.contains("<content>newer</content>"))
|
||||
assertTrue(aggregatedXml.contains("<history_snapshot"))
|
||||
assertTrue(aggregatedXml.contains("<content>older</content>"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -128,7 +132,7 @@ class ContextWorkspaceTest {
|
||||
|
||||
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
|
||||
|
||||
assertEquals(listOf("replacement"), resolved.map { (it as TestBlockContent).content })
|
||||
assertEquals(listOf("replacement"), resolved.blocks.map { (it as TestBlockContent).content })
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -158,7 +162,7 @@ class ContextWorkspaceTest {
|
||||
|
||||
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
|
||||
|
||||
assertEquals(listOf("survivor"), resolved.map { (it as TestBlockContent).content })
|
||||
assertEquals(listOf("survivor"), resolved.blocks.map { (it as TestBlockContent).content })
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -181,7 +185,7 @@ class ContextWorkspaceTest {
|
||||
|
||||
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
|
||||
|
||||
assertEquals(listOf("restored"), resolved.map { (it as TestBlockContent).content })
|
||||
assertEquals(listOf("restored"), resolved.blocks.map { (it as TestBlockContent).content })
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -215,6 +219,185 @@ class ContextWorkspaceTest {
|
||||
assertSame(summary, summaryBlock.render())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `primary exposure renders at least compact and can render full`() {
|
||||
val lowActivationBlock = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "full",
|
||||
compactContent = "compact",
|
||||
abstractContent = "abstract",
|
||||
replaceFadeFactor = 80.0
|
||||
)
|
||||
lowActivationBlock.applyReplaceFade()
|
||||
|
||||
val highActivationBlock = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "full",
|
||||
compactContent = "compact",
|
||||
abstractContent = "abstract"
|
||||
)
|
||||
|
||||
assertContent("compact", lowActivationBlock.render(ContextBlock.Exposure.PRIMARY))
|
||||
assertContent("full", highActivationBlock.render(ContextBlock.Exposure.PRIMARY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `secondary exposure never renders full`() {
|
||||
val block = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "full",
|
||||
compactContent = "compact",
|
||||
abstractContent = "abstract"
|
||||
)
|
||||
|
||||
assertContent("compact", block.render(ContextBlock.Exposure.SECONDARY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `primary exposure promotes abstract only to compact in one activation`() {
|
||||
val block = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "full",
|
||||
compactContent = "compact",
|
||||
abstractContent = "abstract",
|
||||
replaceFadeFactor = 80.0,
|
||||
activateFactor = 50.0
|
||||
)
|
||||
block.applyReplaceFade()
|
||||
|
||||
block.activate(ContextBlock.Exposure.PRIMARY)
|
||||
|
||||
assertContent("compact", block.render())
|
||||
assertContent("compact", block.render(ContextBlock.Exposure.PRIMARY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `primary exposure promotes compact to full`() {
|
||||
val block = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "full",
|
||||
compactContent = "compact",
|
||||
abstractContent = "abstract",
|
||||
replaceFadeFactor = 40.0,
|
||||
activateFactor = 20.0
|
||||
)
|
||||
block.applyReplaceFade()
|
||||
|
||||
block.activate(ContextBlock.Exposure.PRIMARY)
|
||||
|
||||
assertContent("full", block.render())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `secondary exposure keeps abstract within abstract tier`() {
|
||||
val block = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "full",
|
||||
compactContent = "compact",
|
||||
abstractContent = "abstract",
|
||||
replaceFadeFactor = 80.0,
|
||||
activateFactor = 200.0
|
||||
)
|
||||
block.applyReplaceFade()
|
||||
|
||||
block.activate(ContextBlock.Exposure.SECONDARY)
|
||||
|
||||
assertContent("abstract", block.render())
|
||||
assertContent("abstract", block.render(ContextBlock.Exposure.SECONDARY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `secondary exposure keeps compact within compact tier`() {
|
||||
val block = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "full",
|
||||
compactContent = "compact",
|
||||
abstractContent = "abstract",
|
||||
replaceFadeFactor = 40.0,
|
||||
activateFactor = 200.0
|
||||
)
|
||||
block.applyReplaceFade()
|
||||
|
||||
block.activate(ContextBlock.Exposure.SECONDARY)
|
||||
|
||||
assertContent("compact", block.render())
|
||||
assertContent("compact", block.render(ContextBlock.Exposure.SECONDARY))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve uses exposure-specific rendering for primary and secondary domains`() {
|
||||
val manager = ContextWorkspace()
|
||||
val block = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "full",
|
||||
compactContent = "compact",
|
||||
abstractContent = "abstract",
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY, ContextBlock.VisibleDomain.ACTION),
|
||||
replaceFadeFactor = 80.0
|
||||
)
|
||||
block.applyReplaceFade()
|
||||
manager.register(block)
|
||||
|
||||
val primaryResolved = manager.resolve(
|
||||
listOf(ContextBlock.VisibleDomain.MEMORY, ContextBlock.VisibleDomain.ACTION)
|
||||
)
|
||||
val secondaryResolved = manager.resolve(
|
||||
listOf(ContextBlock.VisibleDomain.PERCEIVE, ContextBlock.VisibleDomain.ACTION)
|
||||
)
|
||||
|
||||
assertEquals(listOf("compact"), primaryResolved.blocks.map { (it as TestBlockContent).content })
|
||||
assertEquals(listOf("abstract"), secondaryResolved.blocks.map { (it as TestBlockContent).content })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `aggregated blocks use rendered projection for urgency and snapshot`() {
|
||||
val manager = ContextWorkspace()
|
||||
manager.register(
|
||||
ContextBlock(
|
||||
blockContent = TestBlockContent("memory", "main", "full-critical", BlockContent.Urgency.CRITICAL),
|
||||
compactBlock = TestBlockContent("memory", "main", "compact-low", BlockContent.Urgency.LOW),
|
||||
abstractBlock = TestBlockContent("memory", "main", "abstract-low", BlockContent.Urgency.LOW),
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.ACTION),
|
||||
replaceFadeFactor = 20.0,
|
||||
timeFadeFactor = 0.0,
|
||||
activateFactor = 0.0
|
||||
)
|
||||
)
|
||||
manager.register(
|
||||
ContextBlock(
|
||||
blockContent = TestBlockContent("memory", "main", "full-normal", BlockContent.Urgency.NORMAL),
|
||||
compactBlock = TestBlockContent("memory", "main", "compact-normal", BlockContent.Urgency.NORMAL),
|
||||
abstractBlock = TestBlockContent("memory", "main", "abstract-normal", BlockContent.Urgency.NORMAL),
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.ACTION),
|
||||
replaceFadeFactor = 80.0,
|
||||
timeFadeFactor = 0.0,
|
||||
activateFactor = 0.0
|
||||
)
|
||||
)
|
||||
|
||||
val resolved = manager.resolve(
|
||||
listOf(ContextBlock.VisibleDomain.PERCEIVE, ContextBlock.VisibleDomain.ACTION)
|
||||
)
|
||||
|
||||
val aggregated = resolved.blocks.single()
|
||||
val xml = aggregated.encodeToXmlString()
|
||||
assertEquals(BlockContent.Urgency.NORMAL, aggregated.urgency)
|
||||
assertTrue(xml.contains("<content>compact-low</content>"))
|
||||
assertFalse(xml.contains("<content>full-critical</content>"))
|
||||
}
|
||||
|
||||
private fun assertContent(expected: String, rendered: BlockContent) {
|
||||
assertEquals(expected, (rendered as TestBlockContent).content)
|
||||
}
|
||||
|
||||
private fun contextBlock(
|
||||
blockName: String,
|
||||
source: String,
|
||||
@@ -240,8 +423,9 @@ class ContextWorkspaceTest {
|
||||
private class TestBlockContent(
|
||||
blockName: String,
|
||||
source: String,
|
||||
val content: String
|
||||
) : BlockContent(blockName, source) {
|
||||
val content: String,
|
||||
urgency: Urgency = Urgency.NORMAL
|
||||
) : BlockContent(blockName, source, urgency) {
|
||||
override fun fillXml(document: org.w3c.dom.Document, root: org.w3c.dom.Element) {
|
||||
appendTextElement(document, root, "content", content)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user