mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 16:53:04 +08:00
feat(cognition): add ContextWorkspace to manage context blocks
This commit is contained in:
@@ -11,6 +11,8 @@ public interface CognitionCapability {
|
||||
|
||||
String initiateTurn(String input, String target);
|
||||
|
||||
ContextWorkspace contextWorkspace();
|
||||
|
||||
List<Message> getChatMessages();
|
||||
|
||||
List<Message> snapshotChatMessages();
|
||||
|
||||
@@ -35,9 +35,16 @@ public class CognitionCore extends PartnerCore<CognitionCore> {
|
||||
*/
|
||||
private List<Message> chatMessages = new ArrayList<>();
|
||||
|
||||
private final ContextWorkspace contextWorkspace = new ContextWorkspace();
|
||||
|
||||
public CognitionCore() throws IOException, ClassNotFoundException {
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public ContextWorkspace contextWorkspace() {
|
||||
return contextWorkspace;
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public String initiateTurn(String input, String target) {
|
||||
PartnerRunningFlowContext primaryContext = PartnerRunningFlowContext.Companion.fromSelf(input);
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
package work.slhaf.partner.core.cognition
|
||||
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import java.io.StringWriter
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.transform.OutputKeys
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
import kotlin.concurrent.write
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class ContextWorkspace {
|
||||
|
||||
private val stateSet = mutableSetOf<ContextBlock>()
|
||||
private val lock = ReentrantReadWriteLock()
|
||||
|
||||
/**
|
||||
* 根据传入的 [ContextBlock.VisibleDomain] 列表,获取上下文块
|
||||
* @param domains 需要获取上下文的域列表,顺序将决定权重优先级,按照列表排序将具备线性权重分层,最终反映到 blockContent 列表的排序上
|
||||
*/
|
||||
fun resolve(domains: List<ContextBlock.VisibleDomain>): List<BlockContent> = lock.write {
|
||||
if (domains.isEmpty()) {
|
||||
return@write emptyList()
|
||||
}
|
||||
|
||||
val domainWeights = domains
|
||||
.distinct()
|
||||
.withIndex()
|
||||
.associate { (index, domain) -> domain to (domains.size - index) }
|
||||
|
||||
val activeBlocks = mutableListOf<ResolvedContextBlock>()
|
||||
val iterator = stateSet.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val block = iterator.next()
|
||||
val activationScore = block.applyTimeFade()
|
||||
if (activationScore <= 0.0) {
|
||||
iterator.remove()
|
||||
continue
|
||||
}
|
||||
|
||||
val matchedDomains = block.visibleTo.intersect(domainWeights.keys)
|
||||
if (matchedDomains.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
|
||||
activeBlocks += ResolvedContextBlock(
|
||||
block = block,
|
||||
domainWeight = matchedDomains.sumOf { domainWeights.getValue(it) },
|
||||
activationScore = activationScore
|
||||
)
|
||||
}
|
||||
|
||||
activeBlocks
|
||||
.sortedWith(
|
||||
compareBy<ResolvedContextBlock> { it.domainWeight }
|
||||
.thenBy { it.block.sourceKey.blockName }
|
||||
.thenBy { it.block.sourceKey.source }
|
||||
.thenBy { it.activationScore }
|
||||
.thenBy { it.block.blockContent.encodeToXmlString() }
|
||||
)
|
||||
.map { it.block.render() }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param contextBlock 注册的新上下文块
|
||||
*/
|
||||
fun register(contextBlock: ContextBlock) = lock.write {
|
||||
val iterator = stateSet.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val currentBlock = iterator.next()
|
||||
if (!currentBlock.sameWith(contextBlock)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (currentBlock.applyReplaceFade() <= 0.0) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
stateSet += contextBlock
|
||||
}
|
||||
|
||||
fun expire(blockName: String, source: String) = lock.write {
|
||||
val sourceKey = ContextBlock.SourceKey(blockName, source)
|
||||
val iterator = stateSet.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().sourceKey == sourceKey) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private data class ResolvedContextBlock(
|
||||
val block: ContextBlock,
|
||||
val domainWeight: Int,
|
||||
val activationScore: Double
|
||||
)
|
||||
|
||||
data class ContextBlock @JvmOverloads constructor(
|
||||
val blockContent: BlockContent,
|
||||
val compactBlock: BlockContent = blockContent,
|
||||
val abstractBlock: BlockContent = blockContent,
|
||||
/**
|
||||
* 对哪些域可见
|
||||
*/
|
||||
val visibleTo: Set<VisibleDomain>,
|
||||
|
||||
/**
|
||||
* 新的 [blockContent] 属性与其相同时,发生的衰退步长
|
||||
*/
|
||||
private val replaceFadeFactor: Double,
|
||||
|
||||
/**
|
||||
* 随时间发生的衰退步长,按照分钟定义
|
||||
*/
|
||||
private val timeFadeFactor: Double,
|
||||
|
||||
/**
|
||||
* 触发一次激活时,发生的强化步长
|
||||
*/
|
||||
private val activateFactor: Double
|
||||
) {
|
||||
|
||||
/**
|
||||
* 默认活跃分数,降低至0时将在 [ContextWorkspace] 中移除该 block
|
||||
* 此外还参与到当同源 block 存在时的排序,按该分数升序排列,只影响同源 block 间的顺序
|
||||
*/
|
||||
private var activationScore = 100.0
|
||||
private var lastTouchedAt = Instant.now()
|
||||
|
||||
enum class VisibleDomain {
|
||||
ACTION,
|
||||
MEMORY,
|
||||
PERCEIVE,
|
||||
COGNITION,
|
||||
COMMUNICATION,
|
||||
}
|
||||
|
||||
internal val sourceKey: SourceKey
|
||||
get() = SourceKey(blockContent.blockName, blockContent.source)
|
||||
|
||||
fun applyTimeFade(): Double {
|
||||
val now = Instant.now()
|
||||
val elapsedSeconds = Duration.between(lastTouchedAt, now).toMillis() / 1000.0
|
||||
activationScore = max(0.0, activationScore - elapsedSeconds * (timeFadeFactor / 60.0))
|
||||
lastTouchedAt = now
|
||||
return activationScore
|
||||
}
|
||||
|
||||
fun applyReplaceFade(): Double {
|
||||
applyTimeFade()
|
||||
activationScore = max(0.0, activationScore - replaceFadeFactor)
|
||||
return activationScore
|
||||
}
|
||||
|
||||
fun activate(): Double {
|
||||
applyTimeFade()
|
||||
activationScore = min(100.0, activationScore + activateFactor)
|
||||
return activationScore
|
||||
}
|
||||
|
||||
fun sameWith(contextBlock: ContextBlock): Boolean {
|
||||
return this.sourceKey == contextBlock.sourceKey
|
||||
}
|
||||
|
||||
fun render(): BlockContent {
|
||||
return when {
|
||||
activationScore < 30 -> abstractBlock
|
||||
activationScore < 70 -> compactBlock
|
||||
else -> blockContent
|
||||
}
|
||||
}
|
||||
|
||||
data class SourceKey(
|
||||
val blockName: String,
|
||||
val source: String
|
||||
)
|
||||
}
|
||||
|
||||
abstract class BlockContent protected constructor(
|
||||
val blockName: String,
|
||||
val source: String,
|
||||
) {
|
||||
|
||||
fun encodeToXml(): Element {
|
||||
val document = DocumentBuilderFactory.newInstance()
|
||||
.newDocumentBuilder()
|
||||
.newDocument()
|
||||
|
||||
val root = document.createElement(blockName)
|
||||
root.setAttribute("source", source)
|
||||
document.appendChild(root)
|
||||
|
||||
fillXml(document, root)
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
fun encodeToXmlString(): String {
|
||||
val transformer = TransformerFactory.newInstance().newTransformer().apply {
|
||||
setOutputProperty(OutputKeys.INDENT, "yes")
|
||||
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
|
||||
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
|
||||
}
|
||||
|
||||
return StringWriter().use { writer ->
|
||||
transformer.transform(DOMSource(encodeToXml()), StreamResult(writer))
|
||||
writer.toString()
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun fillXml(document: Document, root: Element)
|
||||
|
||||
protected fun appendTextElement(
|
||||
document: Document,
|
||||
parent: Element,
|
||||
tagName: String,
|
||||
value: Any?
|
||||
): Element {
|
||||
val element = document.createElement(tagName)
|
||||
element.textContent = value?.toString() ?: ""
|
||||
parent.appendChild(element)
|
||||
return element
|
||||
}
|
||||
|
||||
protected fun appendChildElement(
|
||||
document: Document,
|
||||
parent: Element,
|
||||
tagName: String,
|
||||
block: Element.() -> Unit = {}
|
||||
): Element {
|
||||
val element = document.createElement(tagName)
|
||||
parent.appendChild(element)
|
||||
element.block()
|
||||
return element
|
||||
}
|
||||
|
||||
protected fun appendCDataElement(
|
||||
document: Document,
|
||||
parent: Element,
|
||||
tagName: String,
|
||||
value: String?
|
||||
): Element {
|
||||
val element = document.createElement(tagName)
|
||||
element.appendChild(document.createCDATASection(value ?: ""))
|
||||
parent.appendChild(element)
|
||||
return element
|
||||
}
|
||||
|
||||
protected fun <T> appendListElement(
|
||||
document: Document,
|
||||
parent: Element,
|
||||
wrapperTagName: String,
|
||||
itemTagName: String,
|
||||
values: Iterable<T>,
|
||||
block: Element.(T) -> Unit = { value ->
|
||||
textContent = value?.toString() ?: ""
|
||||
}
|
||||
): Element {
|
||||
val wrapper = document.createElement(wrapperTagName)
|
||||
parent.appendChild(wrapper)
|
||||
|
||||
for (value in values) {
|
||||
val item = document.createElement(itemTagName)
|
||||
wrapper.appendChild(item)
|
||||
item.block(value)
|
||||
}
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
protected fun <T> appendRepeatedElements(
|
||||
document: Document,
|
||||
parent: Element,
|
||||
itemTagName: String,
|
||||
values: Iterable<T>,
|
||||
block: Element.(T) -> Unit = { value ->
|
||||
textContent = value?.toString() ?: ""
|
||||
}
|
||||
) {
|
||||
for (value in values) {
|
||||
val item = document.createElement(itemTagName)
|
||||
parent.appendChild(item)
|
||||
item.block(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CommunicationBlockContent(
|
||||
blockName: String,
|
||||
source: String,
|
||||
) : BlockContent(
|
||||
blockName,
|
||||
source,
|
||||
) {
|
||||
|
||||
enum class Projection {
|
||||
SUPPLY,
|
||||
CONTEXT
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package work.slhaf.partner.core.cognition
|
||||
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ContextWorkspaceTest {
|
||||
|
||||
@Test
|
||||
fun `sameWith uses blockName and source only`() {
|
||||
val left = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "left",
|
||||
compactContent = "left-compact",
|
||||
abstractContent = "left-abstract"
|
||||
)
|
||||
val sameSource = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "right"
|
||||
)
|
||||
val differentSource = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "backup",
|
||||
content = "right"
|
||||
)
|
||||
|
||||
assertTrue(left.sameWith(sameSource))
|
||||
assertFalse(left.sameWith(differentSource))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve sorts by accumulated domain weight across sources`() {
|
||||
val manager = ContextWorkspace()
|
||||
val lowWeight = contextBlock(
|
||||
blockName = "low",
|
||||
source = "source-low",
|
||||
content = "low",
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.COGNITION)
|
||||
)
|
||||
val midWeight = contextBlock(
|
||||
blockName = "mid",
|
||||
source = "source-mid",
|
||||
content = "mid",
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
|
||||
)
|
||||
val highWeight = contextBlock(
|
||||
blockName = "high",
|
||||
source = "source-high",
|
||||
content = "high",
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.ACTION, ContextBlock.VisibleDomain.MEMORY)
|
||||
)
|
||||
|
||||
manager.register(highWeight)
|
||||
manager.register(lowWeight)
|
||||
manager.register(midWeight)
|
||||
|
||||
val resolved = manager.resolve(
|
||||
listOf(
|
||||
ContextBlock.VisibleDomain.ACTION,
|
||||
ContextBlock.VisibleDomain.MEMORY,
|
||||
ContextBlock.VisibleDomain.COGNITION
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf("low", "mid", "high"),
|
||||
resolved.map { (it as TestBlockContent).content }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve uses activation score only within same source`() {
|
||||
val manager = ContextWorkspace()
|
||||
val older = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "older",
|
||||
replaceFadeFactor = 20.0,
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
|
||||
)
|
||||
val newer = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "newer",
|
||||
replaceFadeFactor = 20.0,
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
|
||||
)
|
||||
val otherSource = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "secondary",
|
||||
content = "other-source",
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
|
||||
)
|
||||
|
||||
manager.register(older)
|
||||
manager.register(newer)
|
||||
manager.register(otherSource)
|
||||
|
||||
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
|
||||
|
||||
assertEquals(
|
||||
listOf("older", "newer", "other-source"),
|
||||
resolved.map { (it as TestBlockContent).content }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register fades matching source blocks and removes zero score ones`() {
|
||||
val manager = ContextWorkspace()
|
||||
val evicted = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "evicted",
|
||||
replaceFadeFactor = 100.0,
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
|
||||
)
|
||||
val replacement = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "replacement",
|
||||
replaceFadeFactor = 100.0,
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY)
|
||||
)
|
||||
|
||||
manager.register(evicted)
|
||||
manager.register(replacement)
|
||||
|
||||
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
|
||||
|
||||
assertEquals(listOf("replacement"), resolved.map { (it as TestBlockContent).content })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expire removes all matching source blocks`() {
|
||||
val manager = ContextWorkspace()
|
||||
val first = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "first"
|
||||
)
|
||||
val second = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "second"
|
||||
)
|
||||
val survivor = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "secondary",
|
||||
content = "survivor"
|
||||
)
|
||||
|
||||
manager.register(first)
|
||||
manager.register(second)
|
||||
manager.register(survivor)
|
||||
|
||||
manager.expire("memory", "main")
|
||||
|
||||
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
|
||||
|
||||
assertEquals(listOf("survivor"), resolved.map { (it as TestBlockContent).content })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expire allows same source to be registered again`() {
|
||||
val manager = ContextWorkspace()
|
||||
val original = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "original"
|
||||
)
|
||||
val restored = contextBlock(
|
||||
blockName = "memory",
|
||||
source = "main",
|
||||
content = "restored"
|
||||
)
|
||||
|
||||
manager.register(original)
|
||||
manager.expire("memory", "main")
|
||||
manager.register(restored)
|
||||
|
||||
val resolved = manager.resolve(listOf(ContextBlock.VisibleDomain.MEMORY))
|
||||
|
||||
assertEquals(listOf("restored"), resolved.map { (it as TestBlockContent).content })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `render switches projections by activation score`() {
|
||||
val original = TestBlockContent("memory", "main", "full")
|
||||
val compact = TestBlockContent("memory", "main", "compact")
|
||||
val summary = TestBlockContent("memory", "main", "summary")
|
||||
|
||||
val compactBlock = ContextBlock(
|
||||
blockContent = original,
|
||||
compactBlock = compact,
|
||||
abstractBlock = summary,
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY),
|
||||
replaceFadeFactor = 40.0,
|
||||
timeFadeFactor = 0.0,
|
||||
activateFactor = 0.0
|
||||
)
|
||||
compactBlock.applyReplaceFade()
|
||||
assertSame(compact, compactBlock.render())
|
||||
|
||||
val summaryBlock = ContextBlock(
|
||||
blockContent = original,
|
||||
compactBlock = compact,
|
||||
abstractBlock = summary,
|
||||
visibleTo = setOf(ContextBlock.VisibleDomain.MEMORY),
|
||||
replaceFadeFactor = 80.0,
|
||||
timeFadeFactor = 0.0,
|
||||
activateFactor = 0.0
|
||||
)
|
||||
summaryBlock.applyReplaceFade()
|
||||
assertSame(summary, summaryBlock.render())
|
||||
}
|
||||
|
||||
private fun contextBlock(
|
||||
blockName: String,
|
||||
source: String,
|
||||
content: String,
|
||||
compactContent: String = "${content}-compact",
|
||||
abstractContent: String = "${content}-abstract",
|
||||
visibleTo: Set<ContextBlock.VisibleDomain> = setOf(ContextBlock.VisibleDomain.MEMORY),
|
||||
replaceFadeFactor: Double = 10.0,
|
||||
timeFadeFactor: Double = 0.0,
|
||||
activateFactor: Double = 0.0
|
||||
): ContextBlock {
|
||||
return ContextBlock(
|
||||
blockContent = TestBlockContent(blockName, source, content),
|
||||
compactBlock = TestBlockContent(blockName, source, compactContent),
|
||||
abstractBlock = TestBlockContent(blockName, source, abstractContent),
|
||||
visibleTo = visibleTo,
|
||||
replaceFadeFactor = replaceFadeFactor,
|
||||
timeFadeFactor = timeFadeFactor,
|
||||
activateFactor = activateFactor
|
||||
)
|
||||
}
|
||||
|
||||
private class TestBlockContent(
|
||||
blockName: String,
|
||||
source: String,
|
||||
val content: String
|
||||
) : BlockContent(blockName, source) {
|
||||
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