mirror of
https://github.com/slhaf/Partner.git
synced 2026-06-29 02:29:16 +08:00
Compare commits
12 Commits
0567837dfe
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a45cb0a6c8 | |||
| b414a5eb8e | |||
| bc2c993473 | |||
| 0e693310f8 | |||
| 82a33c3909 | |||
| 6a64ff29c4 | |||
| 03f0e1e11f | |||
| 0211ba9ac8 | |||
| 9f9f7247f0 | |||
| 15c24154f8 | |||
| a23657ec0c | |||
| 371b4a01d7 |
@@ -3,10 +3,12 @@ package work.slhaf.partner.core.cognition;
|
|||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
|
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
|
||||||
import work.slhaf.partner.core.cognition.impression.ActiveEntity;
|
import work.slhaf.partner.core.cognition.impression.ActiveEntity;
|
||||||
|
import work.slhaf.partner.core.cognition.impression.Entity;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
|
||||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
|
|
||||||
@@ -29,6 +31,60 @@ public interface CognitionCapability {
|
|||||||
|
|
||||||
Lock getMessageLock();
|
Lock getMessageLock();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project user input onto known or currently active entities and append the input as runtime evidence.
|
||||||
|
*/
|
||||||
Set<ActiveEntity> projectEntity(String input);
|
Set<ActiveEntity> projectEntity(String input);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return current active entities with their bound known entities when available.
|
||||||
|
* ActiveEntity values are snapshots; Entity values are live known-entity references and should be updated through this capability.
|
||||||
|
*/
|
||||||
|
Map<ActiveEntity, Entity> showEntities();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and register a new known entity by subject, then refresh search indexes for it.
|
||||||
|
*/
|
||||||
|
String createEntity(String subject);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a known entity by uuid, or null when it does not exist.
|
||||||
|
*/
|
||||||
|
Entity getEntity(String uuid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate a known entity into the runtime context and return a detached active-entity snapshot.
|
||||||
|
*/
|
||||||
|
ActiveEntity activateKnownEntity(String entityUuid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind an active runtime entity to a known entity and refresh the active-entity search document.
|
||||||
|
*/
|
||||||
|
boolean bindActiveEntity(String runtimeId, String entityUuid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename the canonical subject of a known entity and refresh entity/active-entity indexes.
|
||||||
|
*/
|
||||||
|
boolean renameEntitySubject(String entityUuid, String newSubject, boolean keepOldSubjectAsAlias);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an alias or mention form for a known entity and refresh entity indexes.
|
||||||
|
*/
|
||||||
|
boolean addEntityAlias(String entityUuid, String alias, boolean deprecated);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or replace an impression on a known entity and refresh all entity indexes.
|
||||||
|
*/
|
||||||
|
boolean updateEntityImpression(String entityUuid, String impression, String newImpression, double confidence);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or replace a stable feature on a known entity and refresh all entity indexes.
|
||||||
|
*/
|
||||||
|
boolean updateEntityFeature(String entityUuid, String feature, String newFeature, double confidence);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or update a relation from one known entity to another target and refresh all entity indexes.
|
||||||
|
*/
|
||||||
|
boolean updateEntityRelation(String entityUuid, String target, String relation, double strength);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,23 @@ class ActiveEntity @JvmOverloads constructor(
|
|||||||
impressions.forEach { _projectedImpressions[it.first] = it.second }
|
impressions.forEach { _projectedImpressions[it.first] = it.second }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a detached runtime snapshot for external inspection without exposing mutable internal collections.
|
||||||
|
*/
|
||||||
|
fun snapshot(): ActiveEntity {
|
||||||
|
val copied = ActiveEntity(
|
||||||
|
runtimeId = runtimeId,
|
||||||
|
createdAt = createdAt,
|
||||||
|
boundEntityUuid = boundEntityUuid,
|
||||||
|
_evidences = synchronized(_evidences) { _evidences.toMutableList() },
|
||||||
|
)
|
||||||
|
copied.updateSubject(subject)
|
||||||
|
copied.touch(lastMentionedAt)
|
||||||
|
copied.addProjectedFeatures(*projectedFeatures.entries.map { it.key to it.value }.toTypedArray())
|
||||||
|
copied.addProjectedImpressions(*projectedImpressions.entries.map { it.key to it.value }.toTypedArray())
|
||||||
|
return copied
|
||||||
|
}
|
||||||
|
|
||||||
override fun fillXml(document: Document, root: Element) {
|
override fun fillXml(document: Document, root: Element) {
|
||||||
root.setAttribute("runtime_id", runtimeId)
|
root.setAttribute("runtime_id", runtimeId)
|
||||||
boundEntityUuid?.let { root.setAttribute("bound_entity_uuid", it) }
|
boundEntityUuid?.let { root.setAttribute("bound_entity_uuid", it) }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import work.slhaf.partner.framework.agent.state.State
|
|||||||
import work.slhaf.partner.framework.agent.state.StateSerializable
|
import work.slhaf.partner.framework.agent.state.StateSerializable
|
||||||
import work.slhaf.partner.framework.agent.state.StateValue
|
import work.slhaf.partner.framework.agent.state.StateValue
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
@@ -13,15 +14,67 @@ import kotlin.concurrent.withLock
|
|||||||
|
|
||||||
class Entity @JvmOverloads constructor(
|
class Entity @JvmOverloads constructor(
|
||||||
val uuid: String = UUID.randomUUID().toString(),
|
val uuid: String = UUID.randomUUID().toString(),
|
||||||
val subject: String,
|
subject: String,
|
||||||
private val relations: MutableMap<String, MutableMap<String, Double>> = mutableMapOf(),
|
private val relations: MutableMap<String, MutableMap<String, Double>> = mutableMapOf(),
|
||||||
private val impressions: MutableMap<String, IndexableData> = mutableMapOf(),
|
private val impressions: MutableMap<String, IndexableData> = mutableMapOf(),
|
||||||
private val features: MutableMap<String, IndexableData> = mutableMapOf()
|
private val features: MutableMap<String, IndexableData> = mutableMapOf(),
|
||||||
|
private val aliases: MutableMap<String, AliasMetadata> = mutableMapOf()
|
||||||
) : StateSerializable {
|
) : StateSerializable {
|
||||||
|
|
||||||
|
private var _subject: String = normalizeIdentityText(subject)
|
||||||
|
|
||||||
private val impressionLock = ReentrantLock()
|
private val impressionLock = ReentrantLock()
|
||||||
private val relationLock = ReentrantLock()
|
private val relationLock = ReentrantLock()
|
||||||
private val featureLock = ReentrantLock()
|
private val featureLock = ReentrantLock()
|
||||||
|
private val identityLock = ReentrantLock()
|
||||||
|
|
||||||
|
val subject: String
|
||||||
|
get() = identityLock.withLock { _subject }
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun renameSubject(newSubject: String, keepOldSubjectAsAlias: Boolean = true): Boolean = identityLock.withLock {
|
||||||
|
val normalizedSubject = normalizeIdentityText(newSubject)
|
||||||
|
if (normalizedSubject.isBlank() || normalizedSubject == _subject) {
|
||||||
|
return@withLock false
|
||||||
|
}
|
||||||
|
|
||||||
|
val previousSubject = _subject
|
||||||
|
if (keepOldSubjectAsAlias && previousSubject.isNotBlank()) {
|
||||||
|
aliases[previousSubject] = aliases[previousSubject]?.copy(deprecated = true)
|
||||||
|
?: AliasMetadata(Instant.now(), deprecated = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases.remove(normalizedSubject)
|
||||||
|
_subject = normalizedSubject
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun addAlias(alias: String, deprecated: Boolean = false): Boolean = identityLock.withLock {
|
||||||
|
val normalizedAlias = normalizeIdentityText(alias)
|
||||||
|
if (normalizedAlias.isBlank() || normalizedAlias == _subject) {
|
||||||
|
return@withLock false
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases[normalizedAlias] = aliases[normalizedAlias]?.copy(deprecated = deprecated)
|
||||||
|
?: AliasMetadata(Instant.now(), deprecated)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun showAliases(includeDeprecated: Boolean = false): Set<AliasView> = identityLock.withLock {
|
||||||
|
aliases.asSequence()
|
||||||
|
.filter { (_, metadata) -> includeDeprecated || !metadata.deprecated }
|
||||||
|
.map { (alias, metadata) ->
|
||||||
|
AliasView(alias, metadata.instant, metadata.deprecated)
|
||||||
|
}
|
||||||
|
.sortedWith(compareBy<AliasView> { it.createdAt }.thenBy { it.alias })
|
||||||
|
.toCollection(LinkedHashSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun snapshotAliases(): Map<String, AliasMetadata> = identityLock.withLock {
|
||||||
|
aliases.mapValues { (_, metadata) -> metadata.copy() }
|
||||||
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun updateRelation(
|
fun updateRelation(
|
||||||
@@ -154,18 +207,56 @@ class Entity @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
identityLock.withLock {
|
||||||
|
state.getString("subject")
|
||||||
|
?.let(::normalizeIdentityText)
|
||||||
|
?.takeIf(String::isNotBlank)
|
||||||
|
?.let { _subject = it }
|
||||||
|
}
|
||||||
|
|
||||||
state.getJSONObject("features")?.let { loadedFeatures ->
|
state.getJSONObject("features")?.let { loadedFeatures ->
|
||||||
featureLock.withLock {
|
featureLock.withLock {
|
||||||
features.clear()
|
features.clear()
|
||||||
features.putAll(loadIndexableDataMap(loadedFeatures))
|
features.putAll(loadIndexableDataMap(loadedFeatures))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.getJSONObject("aliases")?.let { loadedAliases ->
|
||||||
|
identityLock.withLock {
|
||||||
|
aliases.clear()
|
||||||
|
loadedAliases.forEach { (alias, metadataValue) ->
|
||||||
|
val normalizedAlias = normalizeIdentityText(alias)
|
||||||
|
if (normalizedAlias.isBlank() || normalizedAlias == _subject) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
val metadata = when (metadataValue) {
|
||||||
|
is JSONObject -> loadAliasMetadata(metadataValue)
|
||||||
|
else -> AliasMetadata(Instant.now(), deprecated = false)
|
||||||
|
}
|
||||||
|
aliases[normalizedAlias] = metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun convert(): State {
|
override fun convert(): State {
|
||||||
val state = State()
|
val state = State()
|
||||||
state.append("uuid", StateValue.str(uuid))
|
state.append("uuid", StateValue.str(uuid))
|
||||||
state.append("subject", StateValue.str(subject))
|
|
||||||
|
val identityState = identityLock.withLock {
|
||||||
|
IdentityState(
|
||||||
|
subject = _subject,
|
||||||
|
aliases = aliases.mapValues { (_, metadata) ->
|
||||||
|
mapOf(
|
||||||
|
"timestamp" to metadata.instant.toEpochMilli(),
|
||||||
|
"deprecated" to metadata.deprecated
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
state.append("subject", StateValue.str(identityState.subject))
|
||||||
|
state.append("aliases", StateValue.obj(identityState.aliases))
|
||||||
|
|
||||||
val relationState = relationLock.withLock {
|
val relationState = relationLock.withLock {
|
||||||
relations.mapValues { (_, relationMap) -> relationMap.toMap() }
|
relations.mapValues { (_, relationMap) -> relationMap.toMap() }
|
||||||
@@ -187,6 +278,22 @@ class Entity @JvmOverloads constructor(
|
|||||||
|
|
||||||
override fun autoLoadOnRegister(): Boolean = false
|
override fun autoLoadOnRegister(): Boolean = false
|
||||||
|
|
||||||
|
private fun normalizeIdentityText(value: String): String =
|
||||||
|
value.replace(IDENTITY_WHITESPACE_REGEX, " ").trim()
|
||||||
|
|
||||||
|
private fun loadAliasMetadata(state: JSONObject): AliasMetadata {
|
||||||
|
val instant = state.getLong("timestamp")
|
||||||
|
?.let(Instant::ofEpochMilli)
|
||||||
|
?: state.getString("instant")
|
||||||
|
?.let { runCatching { Instant.parse(it) }.getOrNull() }
|
||||||
|
?: Instant.now()
|
||||||
|
|
||||||
|
return AliasMetadata(
|
||||||
|
instant = instant,
|
||||||
|
deprecated = state.getBoolean("deprecated") ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadIndexableDataMap(state: JSONObject): Map<String, IndexableData> {
|
private fun loadIndexableDataMap(state: JSONObject): Map<String, IndexableData> {
|
||||||
val loaded = mutableMapOf<String, IndexableData>()
|
val loaded = mutableMapOf<String, IndexableData>()
|
||||||
state.forEach { (key, value) ->
|
state.forEach { (key, value) ->
|
||||||
@@ -269,4 +376,24 @@ class Entity @JvmOverloads constructor(
|
|||||||
val confidence: Double,
|
val confidence: Double,
|
||||||
val vector: FloatArray?
|
val vector: FloatArray?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private data class IdentityState(
|
||||||
|
val subject: String,
|
||||||
|
val aliases: Map<String, Map<String, Any>>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AliasView(
|
||||||
|
val alias: String,
|
||||||
|
val createdAt: Instant,
|
||||||
|
val deprecated: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AliasMetadata(
|
||||||
|
val instant: Instant,
|
||||||
|
val deprecated: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val IDENTITY_WHITESPACE_REGEX = Regex("\\s+")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,219 @@ public class ImpressionCore implements StateSerializable {
|
|||||||
return projected;
|
return projected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出当前已存在的 ActiveEntity 以及对应的 Entity。ActiveEntity 返回快照,Entity 返回当前已知实体引用。
|
||||||
|
*
|
||||||
|
* 注意:外部模块不要直接修改返回的 Entity,否则文本索引 / 向量索引不会刷新。
|
||||||
|
* Impression 更新应走 updateEntity* 系列接口。
|
||||||
|
*
|
||||||
|
* @return ActiveEntity 快照与已绑定 Entity 的映射
|
||||||
|
*/
|
||||||
|
@CapabilityMethod
|
||||||
|
public Map<ActiveEntity, Entity> showEntities() {
|
||||||
|
Map<ActiveEntity, Entity> result = new LinkedHashMap<>();
|
||||||
|
List<ActiveEntity> entities;
|
||||||
|
synchronized (activeEntities) {
|
||||||
|
entities = activeEntities.stream()
|
||||||
|
.sorted(Comparator
|
||||||
|
.comparing(ActiveEntity::getLastMentionedAt)
|
||||||
|
.reversed()
|
||||||
|
.thenComparing(ActiveEntity::getRuntimeId))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ActiveEntity activeEntity : entities) {
|
||||||
|
Entity boundEntity = Optional.ofNullable(activeEntity.getBoundEntityUuid())
|
||||||
|
.map(knownEntitiesByUuid::get)
|
||||||
|
.orElse(null);
|
||||||
|
result.put(activeEntity.snapshot(), boundEntity);
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableMap(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new known entity and make it visible to recall/update indexes immediately.
|
||||||
|
*/
|
||||||
|
@CapabilityMethod
|
||||||
|
public String createEntity(String subject) {
|
||||||
|
if (subject == null || subject.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("subject must not be blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity entity = new Entity(UUID.randomUUID().toString(), subject.trim());
|
||||||
|
entity.register();
|
||||||
|
knownEntitiesByUuid.put(entity.getUuid(), entity);
|
||||||
|
refreshKnownEntityIndexes(entity);
|
||||||
|
return entity.getUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a known entity by stable uuid.
|
||||||
|
*/
|
||||||
|
@CapabilityMethod
|
||||||
|
public Entity getEntity(String uuid) {
|
||||||
|
if (uuid == null || uuid.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return knownEntitiesByUuid.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate a known entity and return a detached snapshot for external consumers.
|
||||||
|
*/
|
||||||
|
@CapabilityMethod
|
||||||
|
public ActiveEntity activateKnownEntity(String entityUuid) {
|
||||||
|
return activateKnownEntityLive(entityUuid)
|
||||||
|
.map(ActiveEntity::snapshot)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind a runtime active entity to a known entity.
|
||||||
|
* This keeps the active entity in current context while giving later updates a stable storage target.
|
||||||
|
*/
|
||||||
|
@CapabilityMethod
|
||||||
|
public boolean bindActiveEntity(String runtimeId, String entityUuid) {
|
||||||
|
if (runtimeId == null || runtimeId.isBlank() || entityUuid == null || entityUuid.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entity entity = knownEntitiesByUuid.get(entityUuid);
|
||||||
|
if (entity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<ActiveEntity> activeEntity = findActiveEntityByRuntimeId(runtimeId);
|
||||||
|
if (activeEntity.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveEntity active = activeEntity.get();
|
||||||
|
active.bindEntity(entityUuid);
|
||||||
|
active.updateSubject(entity.getSubject());
|
||||||
|
refreshActiveEntityTextSearch(active);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename the canonical subject of a known entity and optionally keep its previous subject as a historical alias.
|
||||||
|
*/
|
||||||
|
@CapabilityMethod
|
||||||
|
public boolean renameEntitySubject(String entityUuid, String newSubject, boolean keepOldSubjectAsAlias) {
|
||||||
|
Entity entity = knownEntitiesByUuid.get(entityUuid);
|
||||||
|
if (entity == null || newSubject == null || newSubject.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean renamed = entity.renameSubject(newSubject.trim(), keepOldSubjectAsAlias);
|
||||||
|
if (!renamed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshKnownEntityIndexes(entity);
|
||||||
|
syncBoundActiveEntitySubjects(entity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an alias or mention form for a known entity and refresh search indexes.
|
||||||
|
*/
|
||||||
|
@CapabilityMethod
|
||||||
|
public boolean addEntityAlias(String entityUuid, String alias, boolean deprecated) {
|
||||||
|
Entity entity = knownEntitiesByUuid.get(entityUuid);
|
||||||
|
if (entity == null || alias == null || alias.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean added = entity.addAlias(alias.trim(), deprecated);
|
||||||
|
if (!added) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshKnownEntityIndexes(entity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a known entity impression through the core so text/vector indexes stay consistent.
|
||||||
|
* newImpression can be null or blank to update the existing impression in place.
|
||||||
|
*/
|
||||||
|
@CapabilityMethod
|
||||||
|
public boolean updateEntityImpression(
|
||||||
|
String entityUuid,
|
||||||
|
String impression,
|
||||||
|
String newImpression,
|
||||||
|
double confidence
|
||||||
|
) {
|
||||||
|
Entity entity = knownEntitiesByUuid.get(entityUuid);
|
||||||
|
if (entity == null || impression == null || impression.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.updateImpression(
|
||||||
|
impression.trim(),
|
||||||
|
normalizeNullableText(newImpression),
|
||||||
|
confidence
|
||||||
|
);
|
||||||
|
refreshKnownEntityIndexes(entity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a known entity feature through the core so text/vector indexes stay consistent.
|
||||||
|
* newFeature can be null or blank to update the existing feature in place.
|
||||||
|
*/
|
||||||
|
@CapabilityMethod
|
||||||
|
public boolean updateEntityFeature(
|
||||||
|
String entityUuid,
|
||||||
|
String feature,
|
||||||
|
String newFeature,
|
||||||
|
double confidence
|
||||||
|
) {
|
||||||
|
Entity entity = knownEntitiesByUuid.get(entityUuid);
|
||||||
|
if (entity == null || feature == null || feature.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.updateFeature(
|
||||||
|
feature.trim(),
|
||||||
|
normalizeNullableText(newFeature),
|
||||||
|
confidence
|
||||||
|
);
|
||||||
|
refreshKnownEntityIndexes(entity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a known entity relation through the core so search documents reflect the changed relation.
|
||||||
|
*/
|
||||||
|
@CapabilityMethod
|
||||||
|
public boolean updateEntityRelation(
|
||||||
|
String entityUuid,
|
||||||
|
String target,
|
||||||
|
String relation,
|
||||||
|
double strength
|
||||||
|
) {
|
||||||
|
Entity entity = knownEntitiesByUuid.get(entityUuid);
|
||||||
|
if (entity == null || target == null || target.isBlank() || relation == null || relation.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.updateRelation(target.trim(), relation.trim(), strength);
|
||||||
|
refreshKnownEntityIndexes(entity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize optional replacement text used by update methods.
|
||||||
|
*/
|
||||||
|
private String normalizeNullableText(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private List<EntityAssociationMatch> aggregateMatches(
|
private List<EntityAssociationMatch> aggregateMatches(
|
||||||
List<ImpressionSearchHit> hits,
|
List<ImpressionSearchHit> hits,
|
||||||
int limit
|
int limit
|
||||||
@@ -125,7 +338,7 @@ public class ImpressionCore implements StateSerializable {
|
|||||||
private Optional<ActiveEntity> resolveActiveEntity(ImpressionSearchTarget target) {
|
private Optional<ActiveEntity> resolveActiveEntity(ImpressionSearchTarget target) {
|
||||||
return switch (target.getType()) {
|
return switch (target.getType()) {
|
||||||
case ACTIVE_ENTITY -> findActiveEntityByRuntimeId(target.getId());
|
case ACTIVE_ENTITY -> findActiveEntityByRuntimeId(target.getId());
|
||||||
case ENTITY -> activateKnownEntity(target.getId());
|
case ENTITY -> activateKnownEntityLive(target.getId());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +358,7 @@ public class ImpressionCore implements StateSerializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<ActiveEntity> activateKnownEntity(String uuid) {
|
private Optional<ActiveEntity> activateKnownEntityLive(String uuid) {
|
||||||
Entity knownEntity = knownEntitiesByUuid.get(uuid);
|
Entity knownEntity = knownEntitiesByUuid.get(uuid);
|
||||||
if (knownEntity == null) {
|
if (knownEntity == null) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@@ -183,6 +396,42 @@ public class ImpressionCore implements StateSerializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh every index derived from a known entity after mutation.
|
||||||
|
*/
|
||||||
|
private void refreshKnownEntityIndexes(Entity entity) {
|
||||||
|
vectorIndex.sync(entity);
|
||||||
|
refreshKnownEntityTextSearch(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncBoundActiveEntitySubjects(Entity entity) {
|
||||||
|
List<ActiveEntity> boundEntities;
|
||||||
|
synchronized (activeEntities) {
|
||||||
|
boundEntities = activeEntities.stream()
|
||||||
|
.filter(activeEntity -> entity.getUuid().equals(activeEntity.getBoundEntityUuid()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
boundEntities.forEach(activeEntity -> {
|
||||||
|
activeEntity.updateSubject(entity.getSubject());
|
||||||
|
refreshActiveEntityTextSearch(activeEntity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace text-search documents for one known entity.
|
||||||
|
*/
|
||||||
|
private void refreshKnownEntityTextSearch(Entity entity) {
|
||||||
|
ImpressionSearchTarget target = new ImpressionSearchTarget(
|
||||||
|
ImpressionSearchTarget.Type.ENTITY,
|
||||||
|
entity.getUuid()
|
||||||
|
);
|
||||||
|
textSearch.removeByTarget(target);
|
||||||
|
for (ImpressionSearchDocument document : ImpressionSearchDocuments.INSTANCE.fromEntity(entity)) {
|
||||||
|
textSearch.upsert(document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void rebuildTextSearch() {
|
private void rebuildTextSearch() {
|
||||||
List<ImpressionSearchDocument> documents = new ArrayList<>();
|
List<ImpressionSearchDocument> documents = new ArrayList<>();
|
||||||
knownEntitiesByUuid.values().forEach(entity ->
|
knownEntitiesByUuid.values().forEach(entity ->
|
||||||
@@ -222,6 +471,7 @@ public class ImpressionCore implements StateSerializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Entity entity = new Entity(uuid, subject);
|
Entity entity = new Entity(uuid, subject);
|
||||||
|
entity.register();
|
||||||
entity.load();
|
entity.load();
|
||||||
vectorIndex.sync(entity);
|
vectorIndex.sync(entity);
|
||||||
knownEntitiesByUuid.put(uuid, entity);
|
knownEntitiesByUuid.put(uuid, entity);
|
||||||
|
|||||||
@@ -84,6 +84,18 @@ object ImpressionSearchDocuments {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
entity.showAliases(includeDeprecated = true).forEachIndexed { index, alias ->
|
||||||
|
add(
|
||||||
|
ImpressionSearchDocument(
|
||||||
|
id = "entity:${entity.uuid}:alias:$index",
|
||||||
|
target = target,
|
||||||
|
field = ImpressionSearchField.SUBJECT,
|
||||||
|
text = alias.alias,
|
||||||
|
weight = SUBJECT_WEIGHT * ALIAS_WEIGHT_FACTOR,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
entity.snapshotFeatures().keys.forEachIndexed { index, feature ->
|
entity.snapshotFeatures().keys.forEachIndexed { index, feature ->
|
||||||
add(
|
add(
|
||||||
ImpressionSearchDocument(
|
ImpressionSearchDocument(
|
||||||
@@ -131,6 +143,7 @@ object ImpressionSearchDocuments {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val SUBJECT_WEIGHT = 1.0
|
private const val SUBJECT_WEIGHT = 1.0
|
||||||
|
private const val ALIAS_WEIGHT_FACTOR = 0.9
|
||||||
private const val FEATURE_WEIGHT = 0.85
|
private const val FEATURE_WEIGHT = 0.85
|
||||||
private const val IMPRESSION_WEIGHT = 0.75
|
private const val IMPRESSION_WEIGHT = 0.75
|
||||||
private const val RELATION_WEIGHT = 0.65
|
private const val RELATION_WEIGHT = 0.65
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package work.slhaf.partner.core.memory;
|
package work.slhaf.partner.core.memory;
|
||||||
|
|
||||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
|
||||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
import work.slhaf.partner.framework.agent.support.Result;
|
import work.slhaf.partner.framework.agent.support.Result;
|
||||||
@@ -12,13 +12,13 @@ import java.util.List;
|
|||||||
@Capability(value = "memory")
|
@Capability(value = "memory")
|
||||||
public interface MemoryCapability {
|
public interface MemoryCapability {
|
||||||
|
|
||||||
MemoryUnit getMemoryUnit(String unitId);
|
MemoryUnitSnapshot getMemoryUnit(String unitId);
|
||||||
|
|
||||||
Result<MemorySlice> getMemorySlice(String unitId, String sliceId);
|
Result<MemorySliceSnapshot> getMemorySlice(String unitId, String sliceId);
|
||||||
|
|
||||||
MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary);
|
MemoryUnitSnapshot updateMemoryUnit(List<Message> chatMessages, String summary);
|
||||||
|
|
||||||
Collection<MemoryUnit> listMemoryUnits();
|
Collection<MemoryUnitSnapshot> listMemoryUnits();
|
||||||
|
|
||||||
void refreshMemorySession();
|
void refreshMemorySession();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import com.alibaba.fastjson2.JSONObject;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
|
||||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
@@ -36,10 +38,10 @@ public class MemoryCore implements StateSerializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@CapabilityMethod
|
@CapabilityMethod
|
||||||
public MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary) {
|
public MemoryUnitSnapshot updateMemoryUnit(List<Message> chatMessages, String summary) {
|
||||||
memoryLock.lock();
|
memoryLock.lock();
|
||||||
try {
|
try {
|
||||||
MemoryUnit unit = getMemoryUnit(memorySessionId);
|
MemoryUnit unit = getOrLoadMemoryUnit(memorySessionId);
|
||||||
unit.updateTimestamp();
|
unit.updateTimestamp();
|
||||||
|
|
||||||
List<Message> conversationMessages = unit.getConversationMessages();
|
List<Message> conversationMessages = unit.getConversationMessages();
|
||||||
@@ -55,14 +57,60 @@ public class MemoryCore implements StateSerializable {
|
|||||||
|
|
||||||
unit.getSlices().add(memorySlice);
|
unit.getSlices().add(memorySlice);
|
||||||
normalizeMemoryUnit(unit);
|
normalizeMemoryUnit(unit);
|
||||||
return unit;
|
return unit.snapshot();
|
||||||
} finally {
|
} finally {
|
||||||
memoryLock.unlock();
|
memoryLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CapabilityMethod
|
@CapabilityMethod
|
||||||
public MemoryUnit getMemoryUnit(String unitId) {
|
public MemoryUnitSnapshot getMemoryUnit(String unitId) {
|
||||||
|
memoryLock.lock();
|
||||||
|
try {
|
||||||
|
MemoryUnit unit = getOrLoadMemoryUnit(unitId);
|
||||||
|
normalizeMemoryUnit(unit);
|
||||||
|
return unit.snapshot();
|
||||||
|
} finally {
|
||||||
|
memoryLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CapabilityMethod
|
||||||
|
public Result<MemorySliceSnapshot> getMemorySlice(String unitId, String sliceId) {
|
||||||
|
memoryLock.lock();
|
||||||
|
try {
|
||||||
|
MemoryUnit memoryUnit = memoryUnits.get(unitId);
|
||||||
|
if (memoryUnit == null) {
|
||||||
|
return memorySliceNotFound(unitId, sliceId);
|
||||||
|
}
|
||||||
|
memoryUnit.load();
|
||||||
|
normalizeMemoryUnit(memoryUnit);
|
||||||
|
for (MemorySlice slice : memoryUnit.getSlices()) {
|
||||||
|
if (sliceId.equals(slice.getId())) {
|
||||||
|
return Result.success(slice.snapshot());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return memorySliceNotFound(unitId, sliceId);
|
||||||
|
} finally {
|
||||||
|
memoryLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CapabilityMethod
|
||||||
|
public Collection<MemoryUnitSnapshot> listMemoryUnits() {
|
||||||
|
memoryLock.lock();
|
||||||
|
try {
|
||||||
|
return memoryUnits.values().stream()
|
||||||
|
.peek(MemoryUnit::load)
|
||||||
|
.peek(this::normalizeMemoryUnit)
|
||||||
|
.map(MemoryUnit::snapshot)
|
||||||
|
.toList();
|
||||||
|
} finally {
|
||||||
|
memoryLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MemoryUnit getOrLoadMemoryUnit(String unitId) {
|
||||||
MemoryUnit unit = memoryUnits.computeIfAbsent(unitId, id -> {
|
MemoryUnit unit = memoryUnits.computeIfAbsent(unitId, id -> {
|
||||||
MemoryUnit newUnit = new MemoryUnit(id);
|
MemoryUnit newUnit = new MemoryUnit(id);
|
||||||
newUnit.register();
|
newUnit.register();
|
||||||
@@ -72,21 +120,7 @@ public class MemoryCore implements StateSerializable {
|
|||||||
return unit;
|
return unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@CapabilityMethod
|
private Result<MemorySliceSnapshot> memorySliceNotFound(String unitId, String sliceId) {
|
||||||
public Result<MemorySlice> getMemorySlice(String unitId, String sliceId) {
|
|
||||||
MemoryUnit memoryUnit = memoryUnits.get(unitId);
|
|
||||||
if (memoryUnit == null) {
|
|
||||||
return Result.failure(new MemoryLookupException(
|
|
||||||
"Memory slice not found: " + unitId + ":" + sliceId,
|
|
||||||
unitId + ":" + sliceId,
|
|
||||||
"MEMORY_SLICE"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
for (MemorySlice slice : memoryUnit.getSlices()) {
|
|
||||||
if (sliceId.equals(slice.getId())) {
|
|
||||||
return Result.success(slice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Result.failure(new MemoryLookupException(
|
return Result.failure(new MemoryLookupException(
|
||||||
"Memory slice not found: " + unitId + ":" + sliceId,
|
"Memory slice not found: " + unitId + ":" + sliceId,
|
||||||
unitId + ":" + sliceId,
|
unitId + ":" + sliceId,
|
||||||
@@ -94,11 +128,6 @@ public class MemoryCore implements StateSerializable {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@CapabilityMethod
|
|
||||||
public Collection<MemoryUnit> listMemoryUnits() {
|
|
||||||
return new ArrayList<>(memoryUnits.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
@CapabilityMethod
|
@CapabilityMethod
|
||||||
public void refreshMemorySession() {
|
public void refreshMemorySession() {
|
||||||
memorySessionId = UUID.randomUUID().toString();
|
memorySessionId = UUID.randomUUID().toString();
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ public class MemorySlice implements Comparable<MemorySlice> {
|
|||||||
return new MemorySlice(id, startIndex, endIndex, summary, timestamp);
|
return new MemorySlice(id, startIndex, endIndex, summary, timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MemorySliceSnapshot snapshot() {
|
||||||
|
return new MemorySliceSnapshot(
|
||||||
|
id,
|
||||||
|
startIndex == null ? 0 : startIndex,
|
||||||
|
endIndex == null ? 0 : endIndex,
|
||||||
|
summary,
|
||||||
|
timestamp == null ? 0L : timestamp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(MemorySlice memorySlice) {
|
public int compareTo(MemorySlice memorySlice) {
|
||||||
if (memorySlice.getTimestamp() > this.getTimestamp()) {
|
if (memorySlice.getTimestamp() > this.getTimestamp()) {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package work.slhaf.partner.core.memory.pojo
|
||||||
|
|
||||||
|
data class MemorySliceSnapshot(
|
||||||
|
val id: String,
|
||||||
|
val startIndex: Int,
|
||||||
|
val endIndex: Int,
|
||||||
|
val summary: String?,
|
||||||
|
val timestamp: Long,
|
||||||
|
)
|
||||||
@@ -31,6 +31,15 @@ public class MemoryUnit implements StateSerializable {
|
|||||||
timestamp = System.currentTimeMillis();
|
timestamp = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MemoryUnitSnapshot snapshot() {
|
||||||
|
return new MemoryUnitSnapshot(
|
||||||
|
id,
|
||||||
|
List.copyOf(conversationMessages),
|
||||||
|
timestamp == null ? 0L : timestamp,
|
||||||
|
slices.stream().map(MemorySlice::snapshot).toList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull Path statePath() {
|
public @NotNull Path statePath() {
|
||||||
return Path.of("core", "memory", "memory-unit" + id + ".json");
|
return Path.of("core", "memory", "memory-unit" + id + ".json");
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package work.slhaf.partner.core.memory.pojo
|
||||||
|
|
||||||
|
import work.slhaf.partner.framework.agent.model.pojo.Message
|
||||||
|
|
||||||
|
data class MemoryUnitSnapshot(
|
||||||
|
val id: String,
|
||||||
|
val conversationMessages: List<Message>,
|
||||||
|
val timestamp: Long,
|
||||||
|
val slices: List<MemorySliceSnapshot>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun messagesOf(slice: MemorySliceSnapshot): List<Message> {
|
||||||
|
if (conversationMessages.isEmpty()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
val start = slice.startIndex.coerceIn(0, conversationMessages.size)
|
||||||
|
val end = slice.endIndex.coerceIn(start, conversationMessages.size)
|
||||||
|
if (start >= end) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
return conversationMessages.subList(start, end).toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ import work.slhaf.partner.core.cognition.CognitionCapability;
|
|||||||
import work.slhaf.partner.core.cognition.context.BlockContent;
|
import work.slhaf.partner.core.cognition.context.BlockContent;
|
||||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||||
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
|
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
|
||||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||||
@@ -75,16 +75,16 @@ class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
|
|||||||
Function<Map<String, Object>, String> invoker = params -> {
|
Function<Map<String, Object>, String> invoker = params -> {
|
||||||
String unitId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "unit_id");
|
String unitId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "unit_id");
|
||||||
String sliceId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "slice_id");
|
String sliceId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "slice_id");
|
||||||
Result<MemorySlice> sliceResult = memoryCapability.getMemorySlice(unitId, sliceId);
|
Result<MemorySliceSnapshot> sliceResult = memoryCapability.getMemorySlice(unitId, sliceId);
|
||||||
if (sliceResult.exceptionOrNull() != null) {
|
if (sliceResult.exceptionOrNull() != null) {
|
||||||
return JSONObject.of(
|
return JSONObject.of(
|
||||||
"ok", false,
|
"ok", false,
|
||||||
"message", sliceResult.exceptionOrNull().getLocalizedMessage()
|
"message", sliceResult.exceptionOrNull().getLocalizedMessage()
|
||||||
).toJSONString();
|
).toJSONString();
|
||||||
}
|
}
|
||||||
MemorySlice slice = sliceResult.getOrThrow();
|
MemorySliceSnapshot slice = sliceResult.getOrThrow();
|
||||||
|
|
||||||
MemoryUnit unit = memoryCapability.getMemoryUnit(unitId);
|
MemoryUnitSnapshot unit = memoryCapability.getMemoryUnit(unitId);
|
||||||
cognitionCapability.contextWorkspace().register(new ContextBlock(
|
cognitionCapability.contextWorkspace().register(new ContextBlock(
|
||||||
buildMemoryRecallFullBlock(unit, slice),
|
buildMemoryRecallFullBlock(unit, slice),
|
||||||
Set.of(ContextBlock.FocusedDomain.MEMORY),
|
Set.of(ContextBlock.FocusedDomain.MEMORY),
|
||||||
@@ -105,13 +105,13 @@ class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull BlockContent buildMemoryRecallFullBlock(MemoryUnit unit, MemorySlice slice) {
|
private @NotNull BlockContent buildMemoryRecallFullBlock(MemoryUnitSnapshot unit, MemorySliceSnapshot slice) {
|
||||||
return new BlockContent("memory_recall", "memory_capability") {
|
return new BlockContent("memory_recall", "memory_capability") {
|
||||||
@Override
|
@Override
|
||||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||||
root.setAttribute("unit_id", unit.getId());
|
root.setAttribute("unit_id", unit.getId());
|
||||||
root.setAttribute("slice_id", slice.getId());
|
root.setAttribute("slice_id", slice.getId());
|
||||||
appendRepeatedElements(document, root, "message", unit.getConversationMessages().subList(slice.getStartIndex(), slice.getEndIndex()), (messageElement, message) -> {
|
appendRepeatedElements(document, root, "message", unit.messagesOf(slice), (messageElement, message) -> {
|
||||||
messageElement.setAttribute("role", message.getRole().name().toLowerCase(Locale.ROOT));
|
messageElement.setAttribute("role", message.getRole().name().toLowerCase(Locale.ROOT));
|
||||||
messageElement.setTextContent(message.getContent());
|
messageElement.setTextContent(message.getContent());
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import work.slhaf.partner.core.cognition.CognitionCapability;
|
|||||||
import work.slhaf.partner.core.cognition.context.BlockContent;
|
import work.slhaf.partner.core.cognition.context.BlockContent;
|
||||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
|
||||||
import work.slhaf.partner.core.perceive.PerceiveCapability;
|
import work.slhaf.partner.core.perceive.PerceiveCapability;
|
||||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||||
@@ -31,6 +31,7 @@ import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
|||||||
|
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
@@ -140,7 +141,7 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
|
|||||||
if (memoryId.isBlank()) {
|
if (memoryId.isBlank()) {
|
||||||
return fullChatSnapshot;
|
return fullChatSnapshot;
|
||||||
}
|
}
|
||||||
MemoryUnit existingUnit = memoryCapability.getMemoryUnit(memoryId);
|
MemoryUnitSnapshot existingUnit = memoryCapability.getMemoryUnit(memoryId);
|
||||||
if (existingUnit.getConversationMessages().isEmpty()) {
|
if (existingUnit.getConversationMessages().isEmpty()) {
|
||||||
return fullChatSnapshot;
|
return fullChatSnapshot;
|
||||||
}
|
}
|
||||||
@@ -158,8 +159,9 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
RollingResult buildRollingResult(List<Message> chatSnapshot, int rollingSize, int retainDivisor) {
|
RollingResult buildRollingResult(List<Message> chatSnapshot, int rollingSize, int retainDivisor) {
|
||||||
messageCompressor.execute(chatSnapshot);
|
List<Message> rollingMessages = new ArrayList<>(chatSnapshot);
|
||||||
Result<String> summaryResult = messageSummarizer.execute(chatSnapshot);
|
messageCompressor.execute(rollingMessages);
|
||||||
|
Result<String> summaryResult = messageSummarizer.execute(rollingMessages);
|
||||||
String summary = summaryResult.fold(
|
String summary = summaryResult.fold(
|
||||||
value -> value,
|
value -> value,
|
||||||
exp -> "no summary, due to exception"
|
exp -> "no summary, due to exception"
|
||||||
@@ -167,20 +169,20 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
|
|||||||
if (summary.isBlank()) {
|
if (summary.isBlank()) {
|
||||||
summary = "no summary, due to empty summarize result";
|
summary = "no summary, due to empty summarize result";
|
||||||
}
|
}
|
||||||
MemoryUnit memoryUnit = memoryCapability.updateMemoryUnit(chatSnapshot, summary);
|
MemoryUnitSnapshot memoryUnit = memoryCapability.updateMemoryUnit(rollingMessages, summary);
|
||||||
MemorySlice newSlice = memoryUnit.getSlices().getLast();
|
MemorySliceSnapshot newSlice = memoryUnit.getSlices().getLast();
|
||||||
return new RollingResult(memoryUnit, newSlice, List.copyOf(chatSnapshot), newSlice.getSummary(), rollingSize, retainDivisor);
|
return new RollingResult(memoryUnit, newSlice, rollingSize, retainDivisor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyRolling(RollingResult result) {
|
private void applyRolling(RollingResult result) {
|
||||||
cognitionCapability.contextWorkspace().register(new ContextBlock(
|
cognitionCapability.contextWorkspace().register(new ContextBlock(
|
||||||
buildDialogAbstractBlock(result.summary(), result.memoryUnit().getId(), result.memorySlice().getId()),
|
buildDialogAbstractBlock(result.getSummary(), result.getMemoryUnit().getId(), result.getMemorySlice().getId()),
|
||||||
Set.of(ContextBlock.FocusedDomain.MEMORY, ContextBlock.FocusedDomain.COMMUNICATION),
|
Set.of(ContextBlock.FocusedDomain.MEMORY, ContextBlock.FocusedDomain.COMMUNICATION),
|
||||||
20,
|
20,
|
||||||
5,
|
5,
|
||||||
10
|
10
|
||||||
));
|
));
|
||||||
cognitionCapability.rollChatMessagesWithSnapshot(result.rollingSize(), result.retainDivisor());
|
cognitionCapability.rollChatMessagesWithSnapshot(result.getRollingSize(), result.getRetainDivisor());
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull BlockContent buildDialogAbstractBlock(String summary, String unitId, String sliceId) {
|
private @NotNull BlockContent buildDialogAbstractBlock(String summary, String unitId, String sliceId) {
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package work.slhaf.partner.module.communication;
|
|
||||||
|
|
||||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
|
||||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
|
||||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public record RollingResult(
|
|
||||||
MemoryUnit memoryUnit,
|
|
||||||
MemorySlice memorySlice,
|
|
||||||
List<Message> incrementMessages,
|
|
||||||
String summary,
|
|
||||||
int rollingSize,
|
|
||||||
int retainDivisor
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package work.slhaf.partner.module.communication
|
||||||
|
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot
|
||||||
|
import work.slhaf.partner.framework.agent.model.pojo.Message
|
||||||
|
|
||||||
|
data class RollingResult(
|
||||||
|
val memoryUnit: MemoryUnitSnapshot,
|
||||||
|
val memorySlice: MemorySliceSnapshot,
|
||||||
|
val rollingSize: Int,
|
||||||
|
val retainDivisor: Int,
|
||||||
|
) {
|
||||||
|
val summary: String
|
||||||
|
get() = memorySlice.summary ?: ""
|
||||||
|
|
||||||
|
fun incrementMessages(): List<Message> = memoryUnit.messagesOf(memorySlice)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package work.slhaf.partner.module.impression
|
||||||
|
|
||||||
|
data class ImpressionEntityObservation @JvmOverloads constructor(
|
||||||
|
val proposedSubject: String,
|
||||||
|
val aliases: List<String> = emptyList(),
|
||||||
|
val impressions: Map<String, Int> = emptyMap(),
|
||||||
|
val features: Map<String, Int> = emptyMap(),
|
||||||
|
val relations: Map<String, Map<String, Int>> = emptyMap(),
|
||||||
|
val sourceActiveRuntimeIds: List<String> = emptyList(),
|
||||||
|
val evidenceSnippets: List<String> = emptyList(),
|
||||||
|
val reason: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class KnownEntityIdentity @JvmOverloads constructor(
|
||||||
|
val entityUuid: String,
|
||||||
|
val subject: String,
|
||||||
|
val aliases: List<String> = emptyList(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package work.slhaf.partner.module.impression;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record ImpressionUpdateApplyResult(
|
||||||
|
List<String> createdEntityUuids
|
||||||
|
) {
|
||||||
|
|
||||||
|
public ImpressionUpdateApplyResult {
|
||||||
|
createdEntityUuids = createdEntityUuids == null ? List.of() : List.copyOf(createdEntityUuids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImpressionUpdateApplyResult empty() {
|
||||||
|
return new ImpressionUpdateApplyResult(List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package work.slhaf.partner.module.impression;
|
||||||
|
|
||||||
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record ImpressionUpdateContext(
|
||||||
|
String memoryUnitId,
|
||||||
|
String memorySliceId,
|
||||||
|
String summary,
|
||||||
|
int rollingSize,
|
||||||
|
int retainDivisor,
|
||||||
|
int sliceStartIndex,
|
||||||
|
int sliceEndIndex,
|
||||||
|
long sliceTimestamp,
|
||||||
|
long unitTimestamp,
|
||||||
|
List<Message> incrementMessages
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package work.slhaf.partner.module.impression
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A conservative, auditable plan produced after message rolling.
|
||||||
|
*
|
||||||
|
* The updater should treat this model as intent only: validation decides whether
|
||||||
|
* a step is safe to execute, and the applier performs mutations through
|
||||||
|
* CognitionCapability / ImpressionCore so indexes stay consistent.
|
||||||
|
*/
|
||||||
|
data class ImpressionUpdatePlan @JvmOverloads constructor(
|
||||||
|
val steps: List<ImpressionUpdateStep>,
|
||||||
|
val status: PlanStatus = PlanStatus.PREPARED,
|
||||||
|
val reason: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class PlanStatus {
|
||||||
|
PREPARED,
|
||||||
|
CONFIRMED,
|
||||||
|
REJECTED,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ImpressionUpdateStep
|
||||||
|
|
||||||
|
data class UpdateExistingStep(
|
||||||
|
val entityUuid: String,
|
||||||
|
val updatePatch: UpdatePatch,
|
||||||
|
) : ImpressionUpdateStep()
|
||||||
|
|
||||||
|
|
||||||
|
data class CreateEntityStep(
|
||||||
|
val subject: String,
|
||||||
|
val impressions: List<ImpressionPatch> = emptyList(),
|
||||||
|
val features: List<FeaturePatch> = emptyList(),
|
||||||
|
val aliases: List<AliasPatch> = emptyList(),
|
||||||
|
val relations: List<RelationPatch> = emptyList(),
|
||||||
|
) : ImpressionUpdateStep()
|
||||||
|
|
||||||
|
sealed class UpdatePatch
|
||||||
|
|
||||||
|
data class ImpressionPatch @JvmOverloads constructor(
|
||||||
|
val impression: String,
|
||||||
|
val newImpression: String? = null,
|
||||||
|
val confidence: Double = 1.0,
|
||||||
|
) : UpdatePatch()
|
||||||
|
|
||||||
|
data class FeaturePatch @JvmOverloads constructor(
|
||||||
|
val feature: String,
|
||||||
|
val newFeature: String? = null,
|
||||||
|
val confidence: Double = 1.0,
|
||||||
|
) : UpdatePatch()
|
||||||
|
|
||||||
|
data class AliasPatch @JvmOverloads constructor(
|
||||||
|
val alias: String,
|
||||||
|
val deprecated: Boolean = false,
|
||||||
|
) : UpdatePatch()
|
||||||
|
|
||||||
|
data class SubjectPatch @JvmOverloads constructor(
|
||||||
|
val subject: String,
|
||||||
|
val keepOldSubjectAsAlias: Boolean = true,
|
||||||
|
) : UpdatePatch()
|
||||||
|
|
||||||
|
data class RelationPatch @JvmOverloads constructor(
|
||||||
|
val target: String,
|
||||||
|
val relation: String,
|
||||||
|
val strength: Double = 1.0,
|
||||||
|
) : UpdatePatch()
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package work.slhaf.partner.module.impression;
|
||||||
|
|
||||||
|
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||||
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||||
|
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||||
|
import work.slhaf.partner.framework.agent.support.Result;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ImpressionUpdatePlanApplier extends AbstractAgentModule.Sub<ImpressionUpdatePlan, Result<ImpressionUpdateApplyResult>> {
|
||||||
|
|
||||||
|
@InjectCapability
|
||||||
|
private CognitionCapability cognitionCapability;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Result<ImpressionUpdateApplyResult> doExecute(ImpressionUpdatePlan plan) {
|
||||||
|
return apply(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<ImpressionUpdateApplyResult> apply(ImpressionUpdatePlan plan) {
|
||||||
|
return Result.runCatching(() -> {
|
||||||
|
if (plan == null || plan.getStatus() != PlanStatus.CONFIRMED) {
|
||||||
|
throw new IllegalArgumentException("only confirmed impression update plans can be applied");
|
||||||
|
}
|
||||||
|
List<String> createdEntityUuids = new ArrayList<>();
|
||||||
|
for (ImpressionUpdateStep step : plan.getSteps()) {
|
||||||
|
String createdEntityUuid = applyStep(step);
|
||||||
|
if (createdEntityUuid != null && !createdEntityUuid.isBlank()) {
|
||||||
|
createdEntityUuids.add(createdEntityUuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ImpressionUpdateApplyResult(createdEntityUuids);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String applyStep(ImpressionUpdateStep step) {
|
||||||
|
if (step instanceof UpdateExistingStep updateStep) {
|
||||||
|
applyPatch(updateStep.getEntityUuid(), updateStep.getUpdatePatch());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (step instanceof CreateEntityStep createStep) {
|
||||||
|
String entityUuid = cognitionCapability.createEntity(createStep.getSubject());
|
||||||
|
if (entityUuid == null || entityUuid.isBlank()) {
|
||||||
|
throw new IllegalStateException("created entity uuid is blank");
|
||||||
|
}
|
||||||
|
applyPatches(entityUuid, createStep.getImpressions());
|
||||||
|
applyPatches(entityUuid, createStep.getFeatures());
|
||||||
|
applyPatches(entityUuid, createStep.getAliases());
|
||||||
|
applyPatches(entityUuid, createStep.getRelations());
|
||||||
|
return entityUuid;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("unsupported impression update step: " + step);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPatches(String entityUuid, List<? extends UpdatePatch> patches) {
|
||||||
|
for (UpdatePatch patch : patches) {
|
||||||
|
applyPatch(entityUuid, patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPatch(String entityUuid, UpdatePatch patch) {
|
||||||
|
boolean applied = switch (patch) {
|
||||||
|
case SubjectPatch subjectPatch -> cognitionCapability.renameEntitySubject(
|
||||||
|
entityUuid,
|
||||||
|
subjectPatch.getSubject(),
|
||||||
|
subjectPatch.getKeepOldSubjectAsAlias()
|
||||||
|
);
|
||||||
|
case AliasPatch aliasPatch -> cognitionCapability.addEntityAlias(entityUuid, aliasPatch.getAlias(), aliasPatch.getDeprecated());
|
||||||
|
case ImpressionPatch impressionPatch -> cognitionCapability.updateEntityImpression(
|
||||||
|
entityUuid,
|
||||||
|
impressionPatch.getImpression(),
|
||||||
|
impressionPatch.getNewImpression(),
|
||||||
|
impressionPatch.getConfidence()
|
||||||
|
);
|
||||||
|
case FeaturePatch featurePatch -> cognitionCapability.updateEntityFeature(
|
||||||
|
entityUuid,
|
||||||
|
featurePatch.getFeature(),
|
||||||
|
featurePatch.getNewFeature(),
|
||||||
|
featurePatch.getConfidence()
|
||||||
|
);
|
||||||
|
case RelationPatch relationPatch -> cognitionCapability.updateEntityRelation(
|
||||||
|
entityUuid,
|
||||||
|
relationPatch.getTarget(),
|
||||||
|
relationPatch.getRelation(),
|
||||||
|
relationPatch.getStrength()
|
||||||
|
);
|
||||||
|
case null, default -> throw new IllegalArgumentException("unsupported impression update patch: " + patch);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!applied) {
|
||||||
|
throw new IllegalStateException("failed to apply impression update patch: " + patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package work.slhaf.partner.module.impression;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ImpressionUpdatePlanValidator extends AbstractAgentModule.Sub<ImpressionUpdatePlan, Boolean> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected @NotNull Boolean doExecute(ImpressionUpdatePlan plan) {
|
||||||
|
return isExecutable(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExecutable(ImpressionUpdatePlan plan) {
|
||||||
|
if (plan == null || plan.getStatus() != PlanStatus.PREPARED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<ImpressionUpdateStep> steps = plan.getSteps();
|
||||||
|
if (steps.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (ImpressionUpdateStep step : steps) {
|
||||||
|
if (!isValidStep(step)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidStep(ImpressionUpdateStep step) {
|
||||||
|
if (step instanceof UpdateExistingStep updateStep) {
|
||||||
|
return hasText(updateStep.getEntityUuid()) && isValidPatch(updateStep.getUpdatePatch());
|
||||||
|
}
|
||||||
|
if (step instanceof CreateEntityStep createStep) {
|
||||||
|
return hasText(createStep.getSubject())
|
||||||
|
&& (!createStep.getImpressions().isEmpty()
|
||||||
|
|| !createStep.getFeatures().isEmpty()
|
||||||
|
|| !createStep.getAliases().isEmpty()
|
||||||
|
|| !createStep.getRelations().isEmpty())
|
||||||
|
&& createStep.getImpressions().stream().allMatch(this::isValidPatch)
|
||||||
|
&& createStep.getFeatures().stream().allMatch(this::isValidPatch)
|
||||||
|
&& createStep.getAliases().stream().allMatch(this::isValidPatch)
|
||||||
|
&& createStep.getRelations().stream().allMatch(this::isValidPatch);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidPatch(UpdatePatch patch) {
|
||||||
|
if (patch instanceof ImpressionPatch impressionPatch) {
|
||||||
|
return hasText(impressionPatch.getImpression());
|
||||||
|
}
|
||||||
|
if (patch instanceof FeaturePatch featurePatch) {
|
||||||
|
return hasText(featurePatch.getFeature());
|
||||||
|
}
|
||||||
|
if (patch instanceof AliasPatch aliasPatch) {
|
||||||
|
return hasText(aliasPatch.getAlias());
|
||||||
|
}
|
||||||
|
if (patch instanceof SubjectPatch subjectPatch) {
|
||||||
|
return hasText(subjectPatch.getSubject());
|
||||||
|
}
|
||||||
|
if (patch instanceof RelationPatch relationPatch) {
|
||||||
|
return hasText(relationPatch.getTarget()) && hasText(relationPatch.getRelation());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasText(String value) {
|
||||||
|
return value != null && !value.isBlank();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package work.slhaf.partner.module.impression;
|
||||||
|
|
||||||
|
import kotlin.Unit;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||||
|
import work.slhaf.partner.framework.agent.exception.ModuleExecutionException;
|
||||||
|
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||||
|
import work.slhaf.partner.framework.agent.model.ActivateModel;
|
||||||
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
|
import work.slhaf.partner.framework.agent.support.Result;
|
||||||
|
import work.slhaf.partner.module.TaskBlock;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ImpressionUpdatePlanner extends AbstractAgentModule.Sub<ImpressionUpdateContext, Result<ImpressionUpdatePlan>> implements ActivateModel {
|
||||||
|
|
||||||
|
private static final String MODULE_PROMPT = """
|
||||||
|
你负责在对话 rolling 后,根据新的 memory slice 证据生成保守的实体印象更新计划。
|
||||||
|
|
||||||
|
你只输出 ImpressionUpdatePlan 对应结构:
|
||||||
|
- 如果没有稳定、可复用的实体信息变化,返回 REJECTED 并说明原因。
|
||||||
|
- 只有当证据明确支持时,才返回 PREPARED 计划来创建实体或更新已有实体。
|
||||||
|
- 不要做复杂实体合并,不要发明不在证据中的事实。
|
||||||
|
- patch 字段必须使用简洁、稳定、可索引的表达。
|
||||||
|
- 不要输出 CONFIRMED;CONFIRMED 只能由代码 Validator 通过后设置。
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Result<ImpressionUpdatePlan> doExecute(ImpressionUpdateContext context) {
|
||||||
|
return plan(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<ImpressionUpdatePlan> plan(ImpressionUpdateContext context) {
|
||||||
|
try {
|
||||||
|
return Result.success(formattedChat(List.of(buildTaskMessage(context)), ImpressionUpdatePlan.class).getOrThrow());
|
||||||
|
} catch (AgentRuntimeException e) {
|
||||||
|
return Result.failure(new ModuleExecutionException(
|
||||||
|
"planning impression update failed",
|
||||||
|
this.getClass(),
|
||||||
|
getModuleName()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Message buildTaskMessage(ImpressionUpdateContext context) {
|
||||||
|
return new TaskBlock("impression_update_task") {
|
||||||
|
@Override
|
||||||
|
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||||
|
appendTextElement(document, root, "memory_unit_id", context.memoryUnitId());
|
||||||
|
appendTextElement(document, root, "memory_slice_id", context.memorySliceId());
|
||||||
|
appendTextElement(document, root, "summary", context.summary());
|
||||||
|
appendTextElement(document, root, "rolling_size", Integer.toString(context.rollingSize()));
|
||||||
|
appendTextElement(document, root, "retain_divisor", Integer.toString(context.retainDivisor()));
|
||||||
|
appendTextElement(document, root, "slice_start_index", Integer.toString(context.sliceStartIndex()));
|
||||||
|
appendTextElement(document, root, "slice_end_index", Integer.toString(context.sliceEndIndex()));
|
||||||
|
appendTextElement(document, root, "slice_timestamp", Long.toString(context.sliceTimestamp()));
|
||||||
|
appendTextElement(document, root, "unit_timestamp", Long.toString(context.unitTimestamp()));
|
||||||
|
appendListElement(document, root, "increment_messages", "message", context.incrementMessages(), (element, message) -> {
|
||||||
|
element.setAttribute("role", message.roleValue());
|
||||||
|
element.setTextContent(message.getContent());
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}.encodeToMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String modelKey() {
|
||||||
|
return "impression_update_planner";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull List<Message> modulePrompt() {
|
||||||
|
return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package work.slhaf.partner.module.impression;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||||
|
import work.slhaf.partner.core.cognition.context.BlockContent;
|
||||||
|
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||||
|
import work.slhaf.partner.core.cognition.impression.ActiveEntity;
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot;
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
|
||||||
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||||
|
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||||
|
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||||
|
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||||
|
import work.slhaf.partner.framework.agent.support.Result;
|
||||||
|
import work.slhaf.partner.module.communication.AfterRolling;
|
||||||
|
import work.slhaf.partner.module.communication.AfterRollingRegistry;
|
||||||
|
import work.slhaf.partner.module.communication.RollingResult;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class ImpressionUpdater extends AbstractAgentModule.Standalone implements AfterRolling {
|
||||||
|
|
||||||
|
@InjectCapability
|
||||||
|
private CognitionCapability cognitionCapability;
|
||||||
|
@InjectModule
|
||||||
|
private AfterRollingRegistry afterRollingRegistry;
|
||||||
|
@InjectModule
|
||||||
|
private ImpressionUpdatePlanner planner;
|
||||||
|
@InjectModule
|
||||||
|
private ImpressionUpdatePlanValidator validator;
|
||||||
|
@InjectModule
|
||||||
|
private ImpressionUpdatePlanApplier applier;
|
||||||
|
|
||||||
|
@Init
|
||||||
|
public void init() {
|
||||||
|
afterRollingRegistry.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(RollingResult result) {
|
||||||
|
ImpressionUpdateContext context = buildContext(result);
|
||||||
|
Result<ImpressionUpdatePlan> planResult = planner.execute(context);
|
||||||
|
ImpressionUpdatePlan plan = planResult.getOrDefault(null);
|
||||||
|
if (!validator.execute(plan)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ImpressionUpdatePlan confirmedPlan = new ImpressionUpdatePlan(
|
||||||
|
plan.getSteps(),
|
||||||
|
PlanStatus.CONFIRMED,
|
||||||
|
plan.getReason()
|
||||||
|
);
|
||||||
|
Result<ImpressionUpdateApplyResult> applyResult = applier.execute(confirmedPlan);
|
||||||
|
applyResult.onFailure(exp -> applierFailure(context, exp.getMessage()))
|
||||||
|
.onSuccess(applySummary -> applySummary.createdEntityUuids().forEach(entityUuid -> {
|
||||||
|
ActiveEntity activeEntity = cognitionCapability.activateKnownEntity(entityUuid);
|
||||||
|
if (activeEntity != null) {
|
||||||
|
registerActiveEntity(activeEntity);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImpressionUpdateContext buildContext(RollingResult result) {
|
||||||
|
MemoryUnitSnapshot unit = result.getMemoryUnit();
|
||||||
|
MemorySliceSnapshot slice = result.getMemorySlice();
|
||||||
|
return new ImpressionUpdateContext(
|
||||||
|
unit.getId(),
|
||||||
|
slice.getId(),
|
||||||
|
result.getSummary(),
|
||||||
|
result.getRollingSize(),
|
||||||
|
result.getRetainDivisor(),
|
||||||
|
slice.getStartIndex(),
|
||||||
|
slice.getEndIndex(),
|
||||||
|
slice.getTimestamp(),
|
||||||
|
unit.getTimestamp(),
|
||||||
|
List.copyOf(result.incrementMessages())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applierFailure(ImpressionUpdateContext context, String message) {
|
||||||
|
cognitionCapability.contextWorkspace().register(new ContextBlock(
|
||||||
|
new BlockContent("impression_update_apply_failure", "impression_updater", BlockContent.Urgency.LOW) {
|
||||||
|
@Override
|
||||||
|
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||||
|
appendTextElement(document, root, "memory_unit_id", context.memoryUnitId());
|
||||||
|
appendTextElement(document, root, "memory_slice_id", context.memorySliceId());
|
||||||
|
appendTextElement(document, root, "message", message == null ? "" : message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Set.of(ContextBlock.FocusedDomain.COGNITION),
|
||||||
|
20,
|
||||||
|
20,
|
||||||
|
0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerActiveEntity(ActiveEntity activeEntity) {
|
||||||
|
cognitionCapability.contextWorkspace().register(new ContextBlock(
|
||||||
|
activeEntity,
|
||||||
|
activeEntity,
|
||||||
|
activeEntity,
|
||||||
|
Set.of(ContextBlock.FocusedDomain.COGNITION, ContextBlock.FocusedDomain.MEMORY),
|
||||||
|
100,
|
||||||
|
0.5,
|
||||||
|
20
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,8 +4,8 @@ import com.alibaba.fastjson2.JSONObject;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
|
||||||
import work.slhaf.partner.core.memory.pojo.SliceRef;
|
import work.slhaf.partner.core.memory.pojo.SliceRef;
|
||||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||||
@@ -52,11 +52,11 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void recordMemory(MemoryUnit memoryUnit,
|
public void recordMemory(MemoryUnitSnapshot memoryUnit,
|
||||||
String topicPath,
|
String topicPath,
|
||||||
List<String> relatedTopicPaths,
|
List<String> relatedTopicPaths,
|
||||||
ActivationProfile activationProfile) {
|
ActivationProfile activationProfile) {
|
||||||
MemorySlice memorySlice = memoryUnit.getSlices().getLast();
|
MemorySliceSnapshot memorySlice = memoryUnit.getSlices().getLast();
|
||||||
SliceRef sliceRef = new SliceRef(memoryUnit.getId(), memorySlice.getId());
|
SliceRef sliceRef = new SliceRef(memoryUnit.getId(), memorySlice.getId());
|
||||||
LocalDate date = toLocalDate(memorySlice.getTimestamp());
|
LocalDate date = toLocalDate(memorySlice.getTimestamp());
|
||||||
runtimeLock.lock();
|
runtimeLock.lock();
|
||||||
@@ -159,13 +159,13 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ActivatedMemorySlice buildActivatedMemorySlice(SliceRef ref) {
|
private ActivatedMemorySlice buildActivatedMemorySlice(SliceRef ref) {
|
||||||
MemoryUnit memoryUnit = memoryCapability.getMemoryUnit(ref.getUnitId());
|
MemoryUnitSnapshot memoryUnit = memoryCapability.getMemoryUnit(ref.getUnitId());
|
||||||
Result<MemorySlice> memorySliceResult = memoryCapability.getMemorySlice(ref.getUnitId(), ref.getSliceId());
|
Result<MemorySliceSnapshot> memorySliceResult = memoryCapability.getMemorySlice(ref.getUnitId(), ref.getSliceId());
|
||||||
if (memoryUnit == null || memorySliceResult.exceptionOrNull() != null) {
|
if (memoryUnit == null || memorySliceResult.exceptionOrNull() != null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
MemorySlice memorySlice = memorySliceResult.getOrThrow();
|
MemorySliceSnapshot memorySlice = memorySliceResult.getOrThrow();
|
||||||
List<Message> messages = sliceMessages(memoryUnit, memorySlice);
|
List<Message> messages = memoryUnit.messagesOf(memorySlice);
|
||||||
LocalDate date = toLocalDate(memorySlice.getTimestamp());
|
LocalDate date = toLocalDate(memorySlice.getTimestamp());
|
||||||
return ActivatedMemorySlice.builder()
|
return ActivatedMemorySlice.builder()
|
||||||
.unitId(ref.getUnitId())
|
.unitId(ref.getUnitId())
|
||||||
@@ -177,19 +177,6 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Message> sliceMessages(MemoryUnit memoryUnit, MemorySlice memorySlice) {
|
|
||||||
List<Message> conversationMessages = memoryUnit.getConversationMessages();
|
|
||||||
if (conversationMessages.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
int size = conversationMessages.size();
|
|
||||||
int start = Math.clamp(memorySlice.getStartIndex(), 0, size);
|
|
||||||
int end = Math.clamp(memorySlice.getEndIndex(), start, size);
|
|
||||||
if (start >= end) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
return new ArrayList<>(conversationMessages.subList(start, end));
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocalDate toLocalDate(Long timestamp) {
|
private LocalDate toLocalDate(Long timestamp) {
|
||||||
return Instant.ofEpochMilli(timestamp)
|
return Instant.ofEpochMilli(timestamp)
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ public class MemoryRecallProfileExtractor extends AbstractAgentModule.Standalone
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void consume(RollingResult result) {
|
public void consume(RollingResult result) {
|
||||||
List<Message> slicedMessages = sliceMessages(result);
|
List<Message> slicedMessages = result.incrementMessages();
|
||||||
if (slicedMessages.isEmpty()) {
|
if (slicedMessages.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -169,31 +169,21 @@ public class MemoryRecallProfileExtractor extends AbstractAgentModule.Standalone
|
|||||||
relatedTopicPaths,
|
relatedTopicPaths,
|
||||||
slicedMessages
|
slicedMessages
|
||||||
);
|
);
|
||||||
memoryRuntime.recordMemory(result.memoryUnit(), topicPath, relatedTopicPaths, activationProfile);
|
memoryRuntime.recordMemory(result.getMemoryUnit(), topicPath, relatedTopicPaths, activationProfile);
|
||||||
}).onFailure(exp -> memoryRuntime.recordMemory(
|
}).onFailure(exp -> memoryRuntime.recordMemory(
|
||||||
result.memoryUnit(),
|
result.getMemoryUnit(),
|
||||||
null,
|
null,
|
||||||
List.of(),
|
List.of(),
|
||||||
defaultActivationProfile()
|
defaultActivationProfile()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Message> sliceMessages(RollingResult result) {
|
|
||||||
int size = result.memoryUnit().getConversationMessages().size();
|
|
||||||
int start = Math.clamp(result.memorySlice().getStartIndex(), 0, size);
|
|
||||||
int end = Math.clamp(result.memorySlice().getEndIndex(), start, size);
|
|
||||||
if (start >= end) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
return result.memoryUnit().getConversationMessages().subList(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Message resolveTopicTaskMessage(RollingResult result, List<Message> slicedMessages) {
|
private Message resolveTopicTaskMessage(RollingResult result, List<Message> slicedMessages) {
|
||||||
return new TaskBlock() {
|
return new TaskBlock() {
|
||||||
@Override
|
@Override
|
||||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||||
appendTextElement(document, root, "current_topic_tree", memoryRuntime.getTopicTree());
|
appendTextElement(document, root, "current_topic_tree", memoryRuntime.getTopicTree());
|
||||||
appendTextElement(document, root, "slice_summary", result.summary());
|
appendTextElement(document, root, "slice_summary", result.getSummary());
|
||||||
appendRepeatedElements(document, root, "message", slicedMessages, (messageElement, message) -> {
|
appendRepeatedElements(document, root, "message", slicedMessages, (messageElement, message) -> {
|
||||||
messageElement.setAttribute("role", message.roleValue());
|
messageElement.setAttribute("role", message.roleValue());
|
||||||
messageElement.setTextContent(message.getContent());
|
messageElement.setTextContent(message.getContent());
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Assertions.assertFalse
|
|||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import work.slhaf.partner.core.cognition.impression.ActiveEntity
|
import work.slhaf.partner.core.cognition.impression.ActiveEntity
|
||||||
|
import work.slhaf.partner.core.cognition.impression.Entity
|
||||||
|
|
||||||
class SimpleTextSearchTest {
|
class SimpleTextSearchTest {
|
||||||
|
|
||||||
@@ -116,6 +117,21 @@ class SimpleTextSearchTest {
|
|||||||
assertEquals("report", hits.first().document.target.id)
|
assertEquals("report", hits.first().document.target.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search recalls known entity by alias documents`() {
|
||||||
|
val search = SimpleTextSearch(TestTokenizer())
|
||||||
|
val entity = Entity("entity-1", "Partner")
|
||||||
|
entity.addAlias("智能体项目")
|
||||||
|
|
||||||
|
search.rebuild(ImpressionSearchDocuments.fromEntity(entity))
|
||||||
|
|
||||||
|
val hits = search.search("智能体项目", limit = 10)
|
||||||
|
|
||||||
|
assertFalse(hits.isEmpty())
|
||||||
|
assertEquals(ImpressionSearchTarget.Type.ENTITY, hits.first().document.target.type)
|
||||||
|
assertEquals("entity-1", hits.first().document.target.id)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `upsert replaces previous index terms for the same document id`() {
|
fun `upsert replaces previous index terms for the same document id`() {
|
||||||
val search = SimpleTextSearch(TestTokenizer())
|
val search = SimpleTextSearch(TestTokenizer())
|
||||||
@@ -207,7 +223,8 @@ class SimpleTextSearchTest {
|
|||||||
private val dictionary = listOf(
|
private val dictionary = listOf(
|
||||||
"城南", "旧书店", "老板", "推荐", "工程", "教材", "水利", "熟悉", "旧书",
|
"城南", "旧书店", "老板", "推荐", "工程", "教材", "水利", "熟悉", "旧书",
|
||||||
"java", "kotlin", "jieba", "分词", "simpletextsearch", "倒排", "索引", "检索", "测试", "召回",
|
"java", "kotlin", "jieba", "分词", "simpletextsearch", "倒排", "索引", "检索", "测试", "召回",
|
||||||
"vivado", "实验报告", "实验", "报告", "模板", "docx", "室友", "整理", "文件"
|
"vivado", "实验报告", "实验", "报告", "模板", "docx", "室友", "整理", "文件",
|
||||||
|
"智能体", "项目", "智能体项目"
|
||||||
)
|
)
|
||||||
private val alphaNumericRegex = Regex("[a-z0-9]+(?:[-_./][a-z0-9]+)*")
|
private val alphaNumericRegex = Regex("[a-z0-9]+(?:[-_./][a-z0-9]+)*")
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import org.junit.jupiter.api.BeforeAll;
|
|||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
|
||||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -32,7 +32,7 @@ class MemoryCoreTest {
|
|||||||
void shouldCreateFirstSliceFromChatMessages() {
|
void shouldCreateFirstSliceFromChatMessages() {
|
||||||
String sessionId = memoryCore.getMemorySessionId();
|
String sessionId = memoryCore.getMemorySessionId();
|
||||||
|
|
||||||
MemoryUnit updatedUnit = memoryCore.updateMemoryUnit(List.of(
|
MemoryUnitSnapshot updatedUnit = memoryCore.updateMemoryUnit(List.of(
|
||||||
new Message(Message.Character.USER, "m0"),
|
new Message(Message.Character.USER, "m0"),
|
||||||
new Message(Message.Character.USER, "m1"),
|
new Message(Message.Character.USER, "m1"),
|
||||||
new Message(Message.Character.USER, "m2")
|
new Message(Message.Character.USER, "m2")
|
||||||
@@ -43,7 +43,7 @@ class MemoryCoreTest {
|
|||||||
updatedUnit.getConversationMessages().stream().map(Message::getContent).toList());
|
updatedUnit.getConversationMessages().stream().map(Message::getContent).toList());
|
||||||
assertEquals(1, updatedUnit.getSlices().size());
|
assertEquals(1, updatedUnit.getSlices().size());
|
||||||
|
|
||||||
MemorySlice firstSlice = updatedUnit.getSlices().getFirst();
|
MemorySliceSnapshot firstSlice = updatedUnit.getSlices().getFirst();
|
||||||
assertNotNull(firstSlice.getId());
|
assertNotNull(firstSlice.getId());
|
||||||
assertEquals(0, firstSlice.getStartIndex());
|
assertEquals(0, firstSlice.getStartIndex());
|
||||||
assertEquals(3, firstSlice.getEndIndex());
|
assertEquals(3, firstSlice.getEndIndex());
|
||||||
@@ -60,7 +60,7 @@ class MemoryCoreTest {
|
|||||||
new Message(Message.Character.USER, "m0")
|
new Message(Message.Character.USER, "m0")
|
||||||
), "first-summary");
|
), "first-summary");
|
||||||
|
|
||||||
MemoryUnit updatedUnit = memoryCore.updateMemoryUnit(List.of(
|
MemoryUnitSnapshot updatedUnit = memoryCore.updateMemoryUnit(List.of(
|
||||||
new Message(Message.Character.ASSISTANT, "m1"),
|
new Message(Message.Character.ASSISTANT, "m1"),
|
||||||
new Message(Message.Character.USER, "m2")
|
new Message(Message.Character.USER, "m2")
|
||||||
), "second-summary");
|
), "second-summary");
|
||||||
@@ -70,14 +70,14 @@ class MemoryCoreTest {
|
|||||||
updatedUnit.getConversationMessages().stream().map(Message::getContent).toList());
|
updatedUnit.getConversationMessages().stream().map(Message::getContent).toList());
|
||||||
assertEquals(2, updatedUnit.getSlices().size());
|
assertEquals(2, updatedUnit.getSlices().size());
|
||||||
|
|
||||||
MemorySlice appendedSlice = updatedUnit.getSlices().getLast();
|
MemorySliceSnapshot appendedSlice = updatedUnit.getSlices().getLast();
|
||||||
assertNotNull(appendedSlice.getId());
|
assertNotNull(appendedSlice.getId());
|
||||||
assertEquals(1, appendedSlice.getStartIndex());
|
assertEquals(1, appendedSlice.getStartIndex());
|
||||||
assertEquals(3, appendedSlice.getEndIndex());
|
assertEquals(3, appendedSlice.getEndIndex());
|
||||||
assertEquals("second-summary", appendedSlice.getSummary());
|
assertEquals("second-summary", appendedSlice.getSummary());
|
||||||
assertTrue(appendedSlice.getTimestamp() > 0);
|
assertTrue(appendedSlice.getTimestamp() > 0);
|
||||||
|
|
||||||
MemorySlice loadedSlice = memoryCore.getMemorySlice(sessionId, appendedSlice.getId()).getOrThrow();
|
MemorySliceSnapshot loadedSlice = memoryCore.getMemorySlice(sessionId, appendedSlice.getId()).getOrThrow();
|
||||||
assertNotNull(loadedSlice);
|
assertNotNull(loadedSlice);
|
||||||
assertEquals(1, loadedSlice.getStartIndex());
|
assertEquals(1, loadedSlice.getStartIndex());
|
||||||
assertEquals(3, loadedSlice.getEndIndex());
|
assertEquals(3, loadedSlice.getEndIndex());
|
||||||
|
|||||||
@@ -145,5 +145,63 @@ class CommunicationProducerTest {
|
|||||||
public Lock getMessageLock() {
|
public Lock getMessageLock() {
|
||||||
return lock;
|
return lock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.Set<work.slhaf.partner.core.cognition.impression.ActiveEntity> projectEntity(String input) {
|
||||||
|
return java.util.Set.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.Map<
|
||||||
|
work.slhaf.partner.core.cognition.impression.ActiveEntity,
|
||||||
|
work.slhaf.partner.core.cognition.impression.Entity
|
||||||
|
> showEntities() {
|
||||||
|
return java.util.Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String createEntity(String subject) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public work.slhaf.partner.core.cognition.impression.Entity getEntity(String uuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public work.slhaf.partner.core.cognition.impression.ActiveEntity activateKnownEntity(String entityUuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean bindActiveEntity(String runtimeId, String entityUuid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean renameEntitySubject(String entityUuid, String newSubject, boolean keepOldSubjectAsAlias) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addEntityAlias(String entityUuid, String alias, boolean deprecated) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateEntityImpression(String entityUuid, String impression, String newImpression, double confidence) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateEntityFeature(String entityUuid, String feature, String newFeature, double confidence) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateEntityRelation(String entityUuid, String target, String relation, double strength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import org.junit.jupiter.api.io.TempDir;
|
|||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
|
||||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
import work.slhaf.partner.framework.agent.support.Result;
|
import work.slhaf.partner.framework.agent.support.Result;
|
||||||
import work.slhaf.partner.module.communication.summarizer.MessageCompressor;
|
import work.slhaf.partner.module.communication.summarizer.MessageCompressor;
|
||||||
@@ -63,19 +65,19 @@ class DialogRollingTest {
|
|||||||
message(Message.Character.ASSISTANT, "new-assistant")
|
message(Message.Character.ASSISTANT, "new-assistant")
|
||||||
), 4, 6);
|
), 4, 6);
|
||||||
|
|
||||||
MemoryUnit merged = memoryCapability.getMemoryUnit(sessionId);
|
MemoryUnitSnapshot merged = memoryCapability.getMemoryUnit(sessionId);
|
||||||
assertEquals(List.of("old-user", "old-assistant", "new-user", "new-assistant"),
|
assertEquals(List.of("old-user", "old-assistant", "new-user", "new-assistant"),
|
||||||
merged.getConversationMessages().stream().map(Message::getContent).toList());
|
merged.getConversationMessages().stream().map(Message::getContent).toList());
|
||||||
assertEquals(2, merged.getSlices().size());
|
assertEquals(2, merged.getSlices().size());
|
||||||
|
|
||||||
MemorySlice appendedSlice = merged.getSlices().getLast();
|
MemorySliceSnapshot appendedSlice = merged.getSlices().getLast();
|
||||||
assertNotNull(appendedSlice.getId());
|
assertNotNull(appendedSlice.getId());
|
||||||
assertEquals(2, appendedSlice.getStartIndex());
|
assertEquals(2, appendedSlice.getStartIndex());
|
||||||
assertEquals(4, appendedSlice.getEndIndex());
|
assertEquals(4, appendedSlice.getEndIndex());
|
||||||
assertEquals("new-summary", appendedSlice.getSummary());
|
assertEquals("new-summary", appendedSlice.getSummary());
|
||||||
assertEquals(sessionId, rollingResult.memoryUnit().getId());
|
assertEquals(sessionId, rollingResult.getMemoryUnit().getId());
|
||||||
assertEquals(appendedSlice.getId(), rollingResult.memorySlice().getId());
|
assertEquals(appendedSlice.getId(), rollingResult.getMemorySlice().getId());
|
||||||
assertEquals("new-summary", rollingResult.summary());
|
assertEquals("new-summary", rollingResult.getSummary());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -96,7 +98,7 @@ class DialogRollingTest {
|
|||||||
message(Message.Character.ASSISTANT, "second")
|
message(Message.Character.ASSISTANT, "second")
|
||||||
), 2, 6);
|
), 2, 6);
|
||||||
|
|
||||||
MemoryUnit created = memoryCapability.getMemoryUnit(sessionId);
|
MemoryUnitSnapshot created = memoryCapability.getMemoryUnit(sessionId);
|
||||||
assertNotNull(created);
|
assertNotNull(created);
|
||||||
assertEquals(List.of("first", "second"),
|
assertEquals(List.of("first", "second"),
|
||||||
created.getConversationMessages().stream().map(Message::getContent).toList());
|
created.getConversationMessages().stream().map(Message::getContent).toList());
|
||||||
@@ -104,7 +106,7 @@ class DialogRollingTest {
|
|||||||
assertEquals(0, created.getSlices().getFirst().getStartIndex());
|
assertEquals(0, created.getSlices().getFirst().getStartIndex());
|
||||||
assertEquals(2, created.getSlices().getFirst().getEndIndex());
|
assertEquals(2, created.getSlices().getFirst().getEndIndex());
|
||||||
assertEquals("fresh-summary", created.getSlices().getFirst().getSummary());
|
assertEquals("fresh-summary", created.getSlices().getFirst().getSummary());
|
||||||
assertEquals(created, rollingResult.memoryUnit());
|
assertEquals(created, rollingResult.getMemoryUnit());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -151,8 +153,8 @@ class DialogRollingTest {
|
|||||||
message(Message.Character.ASSISTANT, "a1")
|
message(Message.Character.ASSISTANT, "a1")
|
||||||
), 2, 6);
|
), 2, 6);
|
||||||
|
|
||||||
assertEquals(sessionId, rollingResult.memoryUnit().getId());
|
assertEquals(sessionId, rollingResult.getMemoryUnit().getId());
|
||||||
assertEquals("no summary, due to empty summarize result", rollingResult.summary());
|
assertEquals("no summary, due to empty summarize result", rollingResult.getSummary());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class StubMemoryCapability implements MemoryCapability {
|
private static final class StubMemoryCapability implements MemoryCapability {
|
||||||
@@ -172,28 +174,29 @@ class DialogRollingTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MemoryUnit getMemoryUnit(String unitId) {
|
public MemoryUnitSnapshot getMemoryUnit(String unitId) {
|
||||||
return units.get(unitId);
|
MemoryUnit unit = units.get(unitId);
|
||||||
|
return unit == null ? null : unit.snapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public work.slhaf.partner.framework.agent.support.Result<MemorySlice> getMemorySlice(String unitId, String sliceId) {
|
public work.slhaf.partner.framework.agent.support.Result<MemorySliceSnapshot> getMemorySlice(String unitId, String sliceId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary) {
|
public MemoryUnitSnapshot updateMemoryUnit(List<Message> chatMessages, String summary) {
|
||||||
MemoryUnit unit = units.computeIfAbsent(sessionId, MemoryUnit::new);
|
MemoryUnit unit = units.computeIfAbsent(sessionId, MemoryUnit::new);
|
||||||
unit.updateTimestamp();
|
unit.updateTimestamp();
|
||||||
int startIndex = unit.getConversationMessages().size();
|
int startIndex = unit.getConversationMessages().size();
|
||||||
unit.getConversationMessages().addAll(chatMessages);
|
unit.getConversationMessages().addAll(chatMessages);
|
||||||
unit.getSlices().add(new MemorySlice(startIndex, startIndex + chatMessages.size(), summary));
|
unit.getSlices().add(new MemorySlice(startIndex, startIndex + chatMessages.size(), summary));
|
||||||
return unit;
|
return unit.snapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<MemoryUnit> listMemoryUnits() {
|
public Collection<MemoryUnitSnapshot> listMemoryUnits() {
|
||||||
return units.values();
|
return units.values().stream().map(MemoryUnit::snapshot).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,323 @@
|
|||||||
|
package work.slhaf.partner.module.impression;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||||
|
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
|
||||||
|
import work.slhaf.partner.core.cognition.impression.ActiveEntity;
|
||||||
|
import work.slhaf.partner.core.cognition.impression.Entity;
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||||
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
|
import work.slhaf.partner.framework.agent.support.Result;
|
||||||
|
import work.slhaf.partner.module.communication.AfterRollingRegistry;
|
||||||
|
import work.slhaf.partner.module.communication.RollingResult;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
class ImpressionUpdaterTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void beforeAll(@TempDir Path tempDir) {
|
||||||
|
System.setProperty("user.home", tempDir.toAbsolutePath().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(Object target, String fieldName, Object value) throws Exception {
|
||||||
|
Field field = target.getClass().getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RollingResult rollingResult() {
|
||||||
|
String unitId = "impression-updater-test-" + UUID.randomUUID();
|
||||||
|
MemoryUnit unit = new MemoryUnit(unitId);
|
||||||
|
unit.getConversationMessages().addAll(List.of(
|
||||||
|
new Message(Message.Character.USER, "user likes quiet tools"),
|
||||||
|
new Message(Message.Character.ASSISTANT, "noted")
|
||||||
|
));
|
||||||
|
MemorySlice slice = new MemorySlice(0, 2, "summary");
|
||||||
|
unit.getSlices().add(slice);
|
||||||
|
return new RollingResult(unit.snapshot(), slice.snapshot(), 2, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImpressionUpdatePlan plan(PlanStatus status, ImpressionUpdateStep... steps) {
|
||||||
|
return new ImpressionUpdatePlan(List.of(steps), status, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRegisterItselfToAfterRollingRegistryOnInit() throws Exception {
|
||||||
|
ImpressionUpdater updater = new ImpressionUpdater();
|
||||||
|
AfterRollingRegistry registry = mock(AfterRollingRegistry.class);
|
||||||
|
setField(updater, "afterRollingRegistry", registry);
|
||||||
|
|
||||||
|
updater.init();
|
||||||
|
|
||||||
|
verify(registry).register(updater);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotApplyEmptyPlan() throws Exception {
|
||||||
|
TestApplier applier = new TestApplier(Result.success(ImpressionUpdateApplyResult.empty()));
|
||||||
|
ImpressionUpdater updater = updaterWith(plan(PlanStatus.PREPARED), applier, new RecordingCognitionCapability());
|
||||||
|
|
||||||
|
updater.consume(rollingResult());
|
||||||
|
|
||||||
|
assertEquals(0, applier.applyCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotApplyRejectedOrInvalidPlan() throws Exception {
|
||||||
|
TestApplier applier = new TestApplier(Result.success(ImpressionUpdateApplyResult.empty()));
|
||||||
|
ImpressionUpdater updater = updaterWith(
|
||||||
|
plan(PlanStatus.REJECTED, new UpdateExistingStep("entity-1", new ImpressionPatch("stable"))),
|
||||||
|
applier,
|
||||||
|
new RecordingCognitionCapability()
|
||||||
|
);
|
||||||
|
|
||||||
|
updater.consume(rollingResult());
|
||||||
|
|
||||||
|
assertEquals(0, applier.applyCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldApplyConfirmedUpdateExistingPlanThroughMutationApi() throws Exception {
|
||||||
|
RecordingCognitionCapability cognitionCapability = new RecordingCognitionCapability();
|
||||||
|
ImpressionUpdatePlanApplier applier = new ImpressionUpdatePlanApplier();
|
||||||
|
setField(applier, "cognitionCapability", cognitionCapability);
|
||||||
|
|
||||||
|
Result<ImpressionUpdateApplyResult> result = applier.apply(plan(
|
||||||
|
PlanStatus.CONFIRMED,
|
||||||
|
new UpdateExistingStep("entity-1", new ImpressionPatch("old impression", "new impression", 0.7))
|
||||||
|
));
|
||||||
|
|
||||||
|
assertNull(result.exceptionOrNull());
|
||||||
|
assertEquals("entity-1", cognitionCapability.lastImpressionEntityUuid);
|
||||||
|
assertEquals("old impression", cognitionCapability.lastImpression);
|
||||||
|
assertEquals("new impression", cognitionCapability.lastNewImpression);
|
||||||
|
assertEquals(0.7, cognitionCapability.lastConfidence);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateEntityApplyPatchesActivateAndRegisterActiveSnapshot() throws Exception {
|
||||||
|
RecordingCognitionCapability cognitionCapability = new RecordingCognitionCapability();
|
||||||
|
cognitionCapability.createdEntityUuid = "entity-created";
|
||||||
|
ImpressionUpdatePlanApplier applier = new ImpressionUpdatePlanApplier();
|
||||||
|
setField(applier, "cognitionCapability", cognitionCapability);
|
||||||
|
|
||||||
|
Result<ImpressionUpdateApplyResult> result = applier.apply(plan(
|
||||||
|
PlanStatus.CONFIRMED,
|
||||||
|
new CreateEntityStep(
|
||||||
|
"User",
|
||||||
|
List.of(new ImpressionPatch("prefers concise updates")),
|
||||||
|
List.of(),
|
||||||
|
List.of(new AliasPatch("operator")),
|
||||||
|
List.of()
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
assertNull(result.exceptionOrNull());
|
||||||
|
assertEquals("User", cognitionCapability.createdSubject);
|
||||||
|
assertEquals("entity-created", cognitionCapability.lastImpressionEntityUuid);
|
||||||
|
assertEquals("entity-created", cognitionCapability.lastAliasEntityUuid);
|
||||||
|
assertEquals(List.of("entity-created"), result.getOrThrow().createdEntityUuids());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldConfirmValidatedPreparedPlanAndRegisterCreatedActiveEntity() throws Exception {
|
||||||
|
RecordingCognitionCapability cognitionCapability = new RecordingCognitionCapability();
|
||||||
|
ActiveEntity activeEntity = new ActiveEntity("runtime-1");
|
||||||
|
activeEntity.updateSubject("User");
|
||||||
|
activeEntity.bindEntity("entity-created");
|
||||||
|
cognitionCapability.activatedEntity = activeEntity.snapshot();
|
||||||
|
TestApplier applier = new TestApplier(Result.success(new ImpressionUpdateApplyResult(List.of("entity-created"))));
|
||||||
|
ImpressionUpdater updater = updaterWith(
|
||||||
|
plan(PlanStatus.PREPARED, new CreateEntityStep(
|
||||||
|
"User",
|
||||||
|
List.of(new ImpressionPatch("prefers concise updates")),
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of()
|
||||||
|
)),
|
||||||
|
applier,
|
||||||
|
cognitionCapability
|
||||||
|
);
|
||||||
|
|
||||||
|
updater.consume(rollingResult());
|
||||||
|
|
||||||
|
assertEquals(1, applier.applyCount);
|
||||||
|
assertEquals(PlanStatus.CONFIRMED, applier.lastPlanStatus);
|
||||||
|
assertEquals("entity-created", cognitionCapability.activatedEntityUuid);
|
||||||
|
String resolvedXml = cognitionCapability.contextWorkspace()
|
||||||
|
.resolve(List.of(work.slhaf.partner.core.cognition.context.ContextBlock.FocusedDomain.COGNITION))
|
||||||
|
.encodeToMessage()
|
||||||
|
.getContent();
|
||||||
|
assertTrue(resolvedXml.contains("active_entity_runtime-1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImpressionUpdater updaterWith(ImpressionUpdatePlan plan,
|
||||||
|
ImpressionUpdatePlanApplier applier,
|
||||||
|
CognitionCapability cognitionCapability) throws Exception {
|
||||||
|
ImpressionUpdater updater = new ImpressionUpdater();
|
||||||
|
setField(updater, "cognitionCapability", cognitionCapability);
|
||||||
|
setField(updater, "planner", new TestPlanner(Result.success(plan)));
|
||||||
|
setField(updater, "validator", new ImpressionUpdatePlanValidator());
|
||||||
|
setField(updater, "applier", applier);
|
||||||
|
return updater;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestPlanner extends ImpressionUpdatePlanner {
|
||||||
|
private final Result<ImpressionUpdatePlan> result;
|
||||||
|
|
||||||
|
private TestPlanner(Result<ImpressionUpdatePlan> result) {
|
||||||
|
this.result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<ImpressionUpdatePlan> plan(ImpressionUpdateContext context) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestApplier extends ImpressionUpdatePlanApplier {
|
||||||
|
private final Result<ImpressionUpdateApplyResult> result;
|
||||||
|
private int applyCount;
|
||||||
|
private PlanStatus lastPlanStatus;
|
||||||
|
|
||||||
|
private TestApplier(Result<ImpressionUpdateApplyResult> result) {
|
||||||
|
this.result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<ImpressionUpdateApplyResult> apply(ImpressionUpdatePlan plan) {
|
||||||
|
applyCount++;
|
||||||
|
lastPlanStatus = plan.getStatus();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RecordingCognitionCapability implements CognitionCapability {
|
||||||
|
private final ContextWorkspace contextWorkspace = new ContextWorkspace();
|
||||||
|
private final Lock lock = new ReentrantLock();
|
||||||
|
private String createdEntityUuid = "entity-1";
|
||||||
|
private String createdSubject;
|
||||||
|
private String lastImpressionEntityUuid;
|
||||||
|
private String lastImpression;
|
||||||
|
private String lastNewImpression;
|
||||||
|
private double lastConfidence;
|
||||||
|
private String lastAliasEntityUuid;
|
||||||
|
private String activatedEntityUuid;
|
||||||
|
private ActiveEntity activatedEntity;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initiateTurn(String input, String target, String... skippedModules) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ContextWorkspace contextWorkspace() {
|
||||||
|
return contextWorkspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Message> getChatMessages() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Message> snapshotChatMessages() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void rollChatMessagesWithSnapshot(int snapshotSize, int retainDivisor) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshRecentChatMessagesContext() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Element messageNotesElement() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Lock getMessageLock() {
|
||||||
|
return lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<ActiveEntity> projectEntity(String input) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<ActiveEntity, Entity> showEntities() {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String createEntity(String subject) {
|
||||||
|
createdSubject = subject;
|
||||||
|
return createdEntityUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Entity getEntity(String uuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ActiveEntity activateKnownEntity(String entityUuid) {
|
||||||
|
activatedEntityUuid = entityUuid;
|
||||||
|
return activatedEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean bindActiveEntity(String runtimeId, String entityUuid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean renameEntitySubject(String entityUuid, String newSubject, boolean keepOldSubjectAsAlias) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addEntityAlias(String entityUuid, String alias, boolean deprecated) {
|
||||||
|
lastAliasEntityUuid = entityUuid;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateEntityImpression(String entityUuid, String impression, String newImpression, double confidence) {
|
||||||
|
lastImpressionEntityUuid = entityUuid;
|
||||||
|
lastImpression = impression;
|
||||||
|
lastNewImpression = newImpression;
|
||||||
|
lastConfidence = confidence;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateEntityFeature(String entityUuid, String feature, String newFeature, double confidence) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateEntityRelation(String entityUuid, String target, String relation, double strength) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,9 @@ import work.slhaf.partner.core.cognition.CognitionCapability;
|
|||||||
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
|
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
|
||||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot;
|
||||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||||
|
import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot;
|
||||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||||
import work.slhaf.partner.framework.agent.support.Result;
|
import work.slhaf.partner.framework.agent.support.Result;
|
||||||
import work.slhaf.partner.module.memory.pojo.ActivationProfile;
|
import work.slhaf.partner.module.memory.pojo.ActivationProfile;
|
||||||
@@ -19,7 +21,6 @@ import work.slhaf.partner.module.memory.runtime.exception.MemoryLookupException;
|
|||||||
import work.slhaf.partner.module.memory.selector.ActivatedMemorySlice;
|
import work.slhaf.partner.module.memory.selector.ActivatedMemorySlice;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -41,11 +42,8 @@ class MemoryRuntimeTest {
|
|||||||
System.setProperty("user.home", tempDir.toAbsolutePath().toString());
|
System.setProperty("user.home", tempDir.toAbsolutePath().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
private static List<Message> invokeSliceMessages(MemoryRuntime runtime, MemoryUnit unit, MemorySlice slice) {
|
||||||
private static List<Message> invokeSliceMessages(MemoryRuntime runtime, MemoryUnit unit, MemorySlice slice) throws Exception {
|
return unit.snapshot().messagesOf(slice.snapshot());
|
||||||
Method method = MemoryRuntime.class.getDeclaredMethod("sliceMessages", MemoryUnit.class, MemorySlice.class);
|
|
||||||
method.setAccessible(true);
|
|
||||||
return (List<Message>) method.invoke(runtime, unit, slice);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setField(Object target, String fieldName, Object value) throws Exception {
|
private static void setField(Object target, String fieldName, Object value) throws Exception {
|
||||||
@@ -98,6 +96,64 @@ class MemoryRuntimeTest {
|
|||||||
public Lock getMessageLock() {
|
public Lock getMessageLock() {
|
||||||
return lock;
|
return lock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.Set<work.slhaf.partner.core.cognition.impression.ActiveEntity> projectEntity(String input) {
|
||||||
|
return java.util.Set.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.Map<
|
||||||
|
work.slhaf.partner.core.cognition.impression.ActiveEntity,
|
||||||
|
work.slhaf.partner.core.cognition.impression.Entity
|
||||||
|
> showEntities() {
|
||||||
|
return java.util.Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String createEntity(String subject) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public work.slhaf.partner.core.cognition.impression.Entity getEntity(String uuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public work.slhaf.partner.core.cognition.impression.ActiveEntity activateKnownEntity(String entityUuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean bindActiveEntity(String runtimeId, String entityUuid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean renameEntitySubject(String entityUuid, String newSubject, boolean keepOldSubjectAsAlias) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addEntityAlias(String entityUuid, String alias, boolean deprecated) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateEntityImpression(String entityUuid, String impression, String newImpression, double confidence) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateEntityFeature(String entityUuid, String feature, String newFeature, double confidence) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateEntityRelation(String entityUuid, String target, String relation, double strength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +203,7 @@ class MemoryRuntimeTest {
|
|||||||
unit.getSlices().addAll(List.of(firstSlice, secondSlice));
|
unit.getSlices().addAll(List.of(firstSlice, secondSlice));
|
||||||
memoryCapability.remember(unit);
|
memoryCapability.remember(unit);
|
||||||
|
|
||||||
runtime.recordMemory(unit, "topic/main", List.of("topic/related"), DEFAULT_PROFILE);
|
runtime.recordMemory(unit.snapshot(), "topic/main", List.of("topic/related"), DEFAULT_PROFILE);
|
||||||
|
|
||||||
List<ActivatedMemorySlice> topicResult = runtime.queryActivatedMemoryByTopicPath("topic/main");
|
List<ActivatedMemorySlice> topicResult = runtime.queryActivatedMemoryByTopicPath("topic/main");
|
||||||
assertEquals(List.of("slice-2"), topicResult.stream().map(ActivatedMemorySlice::getSliceId).toList());
|
assertEquals(List.of("slice-2"), topicResult.stream().map(ActivatedMemorySlice::getSliceId).toList());
|
||||||
@@ -187,8 +243,8 @@ class MemoryRuntimeTest {
|
|||||||
relatedUnit.getSlices().add(relatedSlice);
|
relatedUnit.getSlices().add(relatedSlice);
|
||||||
memoryCapability.remember(relatedUnit);
|
memoryCapability.remember(relatedUnit);
|
||||||
|
|
||||||
runtime.recordMemory(mainUnit, "topic/main", List.of("topic/related"), DEFAULT_PROFILE);
|
runtime.recordMemory(mainUnit.snapshot(), "topic/main", List.of("topic/related"), DEFAULT_PROFILE);
|
||||||
runtime.recordMemory(relatedUnit, "topic/related", List.of(), DEFAULT_PROFILE);
|
runtime.recordMemory(relatedUnit.snapshot(), "topic/related", List.of(), DEFAULT_PROFILE);
|
||||||
|
|
||||||
List<ActivatedMemorySlice> topicResult = runtime.queryActivatedMemoryByTopicPath("topic/main");
|
List<ActivatedMemorySlice> topicResult = runtime.queryActivatedMemoryByTopicPath("topic/main");
|
||||||
assertEquals(List.of("slice-main", "slice-related"),
|
assertEquals(List.of("slice-main", "slice-related"),
|
||||||
@@ -207,7 +263,7 @@ class MemoryRuntimeTest {
|
|||||||
MemorySlice firstSlice = MemorySlice.restore("slice-1", 0, 1, "first", 86_400_000L);
|
MemorySlice firstSlice = MemorySlice.restore("slice-1", 0, 1, "first", 86_400_000L);
|
||||||
firstUnitSnapshot.getSlices().add(firstSlice);
|
firstUnitSnapshot.getSlices().add(firstSlice);
|
||||||
memoryCapability.remember(firstUnitSnapshot);
|
memoryCapability.remember(firstUnitSnapshot);
|
||||||
runtime.recordMemory(firstUnitSnapshot, "topic/main", List.of(), DEFAULT_PROFILE);
|
runtime.recordMemory(firstUnitSnapshot.snapshot(), "topic/main", List.of(), DEFAULT_PROFILE);
|
||||||
|
|
||||||
firstUnitSnapshot.getConversationMessages().clear();
|
firstUnitSnapshot.getConversationMessages().clear();
|
||||||
firstUnitSnapshot.getConversationMessages().addAll(List.of(message("m2"), message("m3")));
|
firstUnitSnapshot.getConversationMessages().addAll(List.of(message("m2"), message("m3")));
|
||||||
@@ -215,7 +271,7 @@ class MemoryRuntimeTest {
|
|||||||
firstUnitSnapshot.getSlices().clear();
|
firstUnitSnapshot.getSlices().clear();
|
||||||
firstUnitSnapshot.getSlices().add(secondSlice);
|
firstUnitSnapshot.getSlices().add(secondSlice);
|
||||||
memoryCapability.remember(firstUnitSnapshot);
|
memoryCapability.remember(firstUnitSnapshot);
|
||||||
runtime.recordMemory(firstUnitSnapshot, "topic/main", List.of(), DEFAULT_PROFILE);
|
runtime.recordMemory(firstUnitSnapshot.snapshot(), "topic/main", List.of(), DEFAULT_PROFILE);
|
||||||
|
|
||||||
JSONObject state = JSONObject.parseObject(runtime.convert().toString());
|
JSONObject state = JSONObject.parseObject(runtime.convert().toString());
|
||||||
JSONArray dateIndex = state.getJSONArray("date_index");
|
JSONArray dateIndex = state.getJSONArray("date_index");
|
||||||
@@ -253,14 +309,14 @@ class MemoryRuntimeTest {
|
|||||||
MemorySlice secondSlice = MemorySlice.restore("slice-2", 2, 4, "second", 172_800_000L);
|
MemorySlice secondSlice = MemorySlice.restore("slice-2", 2, 4, "second", 172_800_000L);
|
||||||
mainUnit.getSlices().addAll(List.of(firstSlice, secondSlice));
|
mainUnit.getSlices().addAll(List.of(firstSlice, secondSlice));
|
||||||
memoryCapability.remember(mainUnit);
|
memoryCapability.remember(mainUnit);
|
||||||
runtime.recordMemory(mainUnit, "topic/main", List.of("topic/related"), DEFAULT_PROFILE);
|
runtime.recordMemory(mainUnit.snapshot(), "topic/main", List.of("topic/related"), DEFAULT_PROFILE);
|
||||||
|
|
||||||
MemoryUnit relatedUnit = new MemoryUnit("unit-201");
|
MemoryUnit relatedUnit = new MemoryUnit("unit-201");
|
||||||
relatedUnit.getConversationMessages().addAll(List.of(message("r0"), message("r1")));
|
relatedUnit.getConversationMessages().addAll(List.of(message("r0"), message("r1")));
|
||||||
MemorySlice relatedSlice = MemorySlice.restore("slice-3", 0, 2, "related", 259_200_000L);
|
MemorySlice relatedSlice = MemorySlice.restore("slice-3", 0, 2, "related", 259_200_000L);
|
||||||
relatedUnit.getSlices().add(relatedSlice);
|
relatedUnit.getSlices().add(relatedSlice);
|
||||||
memoryCapability.remember(relatedUnit);
|
memoryCapability.remember(relatedUnit);
|
||||||
runtime.recordMemory(relatedUnit, "topic/related", List.of(), DEFAULT_PROFILE);
|
runtime.recordMemory(relatedUnit.snapshot(), "topic/related", List.of(), DEFAULT_PROFILE);
|
||||||
|
|
||||||
JSONObject state = JSONObject.parseObject(runtime.convert().toString());
|
JSONObject state = JSONObject.parseObject(runtime.convert().toString());
|
||||||
JSONArray topicSlices = state.getJSONArray("topic_slices");
|
JSONArray topicSlices = state.getJSONArray("topic_slices");
|
||||||
@@ -327,21 +383,21 @@ class MemoryRuntimeTest {
|
|||||||
MemorySlice primarySlice = MemorySlice.restore("slice-primary", 0, 2, "primary", System.currentTimeMillis());
|
MemorySlice primarySlice = MemorySlice.restore("slice-primary", 0, 2, "primary", System.currentTimeMillis());
|
||||||
primaryUnit.getSlices().add(primarySlice);
|
primaryUnit.getSlices().add(primarySlice);
|
||||||
memoryCapability.remember(primaryUnit);
|
memoryCapability.remember(primaryUnit);
|
||||||
runtime.recordMemory(primaryUnit, "topic->main", List.of("topic->related"), new ActivationProfile(0.9f, 0.1f, 0.9f));
|
runtime.recordMemory(primaryUnit.snapshot(), "topic->main", List.of("topic->related"), new ActivationProfile(0.9f, 0.1f, 0.9f));
|
||||||
|
|
||||||
MemoryUnit relatedUnit = new MemoryUnit("unit-related-rank");
|
MemoryUnit relatedUnit = new MemoryUnit("unit-related-rank");
|
||||||
relatedUnit.getConversationMessages().addAll(List.of(message("r0"), message("r1")));
|
relatedUnit.getConversationMessages().addAll(List.of(message("r0"), message("r1")));
|
||||||
MemorySlice relatedSlice = MemorySlice.restore("slice-related-rank", 0, 2, "related", System.currentTimeMillis());
|
MemorySlice relatedSlice = MemorySlice.restore("slice-related-rank", 0, 2, "related", System.currentTimeMillis());
|
||||||
relatedUnit.getSlices().add(relatedSlice);
|
relatedUnit.getSlices().add(relatedSlice);
|
||||||
memoryCapability.remember(relatedUnit);
|
memoryCapability.remember(relatedUnit);
|
||||||
runtime.recordMemory(relatedUnit, "topic->related", List.of(), new ActivationProfile(1.0f, 1.0f, 1.0f));
|
runtime.recordMemory(relatedUnit.snapshot(), "topic->related", List.of(), new ActivationProfile(1.0f, 1.0f, 1.0f));
|
||||||
|
|
||||||
MemoryUnit parentUnit = new MemoryUnit("unit-parent");
|
MemoryUnit parentUnit = new MemoryUnit("unit-parent");
|
||||||
parentUnit.getConversationMessages().addAll(List.of(message("x0"), message("x1")));
|
parentUnit.getConversationMessages().addAll(List.of(message("x0"), message("x1")));
|
||||||
MemorySlice parentSlice = MemorySlice.restore("slice-parent", 0, 2, "parent", System.currentTimeMillis());
|
MemorySlice parentSlice = MemorySlice.restore("slice-parent", 0, 2, "parent", System.currentTimeMillis());
|
||||||
parentUnit.getSlices().add(parentSlice);
|
parentUnit.getSlices().add(parentSlice);
|
||||||
memoryCapability.remember(parentUnit);
|
memoryCapability.remember(parentUnit);
|
||||||
runtime.recordMemory(parentUnit, "topic", List.of(), new ActivationProfile(1.0f, 1.0f, 1.0f));
|
runtime.recordMemory(parentUnit.snapshot(), "topic", List.of(), new ActivationProfile(1.0f, 1.0f, 1.0f));
|
||||||
|
|
||||||
List<ActivatedMemorySlice> topicResult = runtime.queryActivatedMemoryByTopicPath("topic->main");
|
List<ActivatedMemorySlice> topicResult = runtime.queryActivatedMemoryByTopicPath("topic->main");
|
||||||
assertEquals(List.of("slice-primary", "slice-related-rank", "slice-parent"),
|
assertEquals(List.of("slice-primary", "slice-related-rank", "slice-parent"),
|
||||||
@@ -361,7 +417,7 @@ class MemoryRuntimeTest {
|
|||||||
primaryUnit.getSlices().add(primarySlice);
|
primaryUnit.getSlices().add(primarySlice);
|
||||||
memoryCapability.remember(primaryUnit);
|
memoryCapability.remember(primaryUnit);
|
||||||
runtime.recordMemory(
|
runtime.recordMemory(
|
||||||
primaryUnit,
|
primaryUnit.snapshot(),
|
||||||
"topic->main",
|
"topic->main",
|
||||||
List.of("topic->related"),
|
List.of("topic->related"),
|
||||||
new ActivationProfile(0.8f, 0.0f, 0.8f)
|
new ActivationProfile(0.8f, 0.0f, 0.8f)
|
||||||
@@ -372,7 +428,7 @@ class MemoryRuntimeTest {
|
|||||||
MemorySlice relatedSlice = MemorySlice.restore("slice-related-zero", 0, 2, "related", System.currentTimeMillis());
|
MemorySlice relatedSlice = MemorySlice.restore("slice-related-zero", 0, 2, "related", System.currentTimeMillis());
|
||||||
relatedUnit.getSlices().add(relatedSlice);
|
relatedUnit.getSlices().add(relatedSlice);
|
||||||
memoryCapability.remember(relatedUnit);
|
memoryCapability.remember(relatedUnit);
|
||||||
runtime.recordMemory(relatedUnit, "topic->related", List.of(), new ActivationProfile(1.0f, 1.0f, 1.0f));
|
runtime.recordMemory(relatedUnit.snapshot(), "topic->related", List.of(), new ActivationProfile(1.0f, 1.0f, 1.0f));
|
||||||
|
|
||||||
List<ActivatedMemorySlice> topicResult = runtime.queryActivatedMemoryByTopicPath("topic->main");
|
List<ActivatedMemorySlice> topicResult = runtime.queryActivatedMemoryByTopicPath("topic->main");
|
||||||
assertEquals(List.of("slice-primary-zero"), topicResult.stream().map(ActivatedMemorySlice::getSliceId).toList());
|
assertEquals(List.of("slice-primary-zero"), topicResult.stream().map(ActivatedMemorySlice::getSliceId).toList());
|
||||||
@@ -391,10 +447,10 @@ class MemoryRuntimeTest {
|
|||||||
unit.getSlices().add(slice);
|
unit.getSlices().add(slice);
|
||||||
memoryCapability.remember(unit);
|
memoryCapability.remember(unit);
|
||||||
|
|
||||||
runtime.recordMemory(unit, "topic->main", List.of("topic->related"), new ActivationProfile(0.2f, 0.1f, 0.2f));
|
runtime.recordMemory(unit.snapshot(), "topic->main", List.of("topic->related"), new ActivationProfile(0.2f, 0.1f, 0.2f));
|
||||||
unit.getSlices().clear();
|
unit.getSlices().clear();
|
||||||
unit.getSlices().add(MemorySlice.restore("slice-refresh", 0, 2, "summary", 172_800_000L));
|
unit.getSlices().add(MemorySlice.restore("slice-refresh", 0, 2, "summary", 172_800_000L));
|
||||||
runtime.recordMemory(unit, "topic->main", List.of("topic->related-2"), new ActivationProfile(0.9f, 0.8f, 0.7f));
|
runtime.recordMemory(unit.snapshot(), "topic->main", List.of("topic->related-2"), new ActivationProfile(0.9f, 0.8f, 0.7f));
|
||||||
|
|
||||||
JSONObject state = JSONObject.parseObject(runtime.convert().toString());
|
JSONObject state = JSONObject.parseObject(runtime.convert().toString());
|
||||||
JSONObject mainTopic = state.getJSONArray("topic_slices").stream()
|
JSONObject mainTopic = state.getJSONArray("topic_slices").stream()
|
||||||
@@ -428,12 +484,13 @@ class MemoryRuntimeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MemoryUnit getMemoryUnit(String unitId) {
|
public MemoryUnitSnapshot getMemoryUnit(String unitId) {
|
||||||
return units.get(unitId);
|
MemoryUnit unit = units.get(unitId);
|
||||||
|
return unit == null ? null : unit.snapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<MemorySlice> getMemorySlice(String unitId, String sliceId) {
|
public Result<MemorySliceSnapshot> getMemorySlice(String unitId, String sliceId) {
|
||||||
MemoryUnit unit = units.get(unitId);
|
MemoryUnit unit = units.get(unitId);
|
||||||
if (unit == null || unit.getSlices() == null) {
|
if (unit == null || unit.getSlices() == null) {
|
||||||
return Result.failure(new MemoryLookupException(
|
return Result.failure(new MemoryLookupException(
|
||||||
@@ -445,7 +502,7 @@ class MemoryRuntimeTest {
|
|||||||
return unit.getSlices().stream()
|
return unit.getSlices().stream()
|
||||||
.filter(slice -> sliceId.equals(slice.getId()))
|
.filter(slice -> sliceId.equals(slice.getId()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.map(Result::success)
|
.map(slice -> Result.success(slice.snapshot()))
|
||||||
.orElseGet(() -> Result.failure(new MemoryLookupException(
|
.orElseGet(() -> Result.failure(new MemoryLookupException(
|
||||||
"Memory slice not found: " + unitId + ":" + sliceId,
|
"Memory slice not found: " + unitId + ":" + sliceId,
|
||||||
unitId + ":" + sliceId,
|
unitId + ":" + sliceId,
|
||||||
@@ -454,13 +511,13 @@ class MemoryRuntimeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MemoryUnit updateMemoryUnit(List<Message> chatMessages, String summary) {
|
public MemoryUnitSnapshot updateMemoryUnit(List<Message> chatMessages, String summary) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<MemoryUnit> listMemoryUnits() {
|
public Collection<MemoryUnitSnapshot> listMemoryUnits() {
|
||||||
return units.values();
|
return units.values().stream().map(MemoryUnit::snapshot).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -79,13 +79,10 @@ class MemoryRecallProfileExtractorTest {
|
|||||||
MemorySlice slice = new MemorySlice(2, 4, "slice-summary");
|
MemorySlice slice = new MemorySlice(2, 4, "slice-summary");
|
||||||
unit.getSlices().add(slice);
|
unit.getSlices().add(slice);
|
||||||
|
|
||||||
updater.consume(new RollingResult(unit, slice, List.of(
|
updater.consume(new RollingResult(unit.snapshot(), slice.snapshot(), 4, 6));
|
||||||
message(Message.Character.USER, "new"),
|
|
||||||
message(Message.Character.ASSISTANT, "new-reply")
|
|
||||||
), "slice-summary", 4, 6));
|
|
||||||
|
|
||||||
verify(memoryRuntime).recordMemory(
|
verify(memoryRuntime).recordMemory(
|
||||||
eq(unit),
|
eq(unit.snapshot()),
|
||||||
eq("root->branch"),
|
eq("root->branch"),
|
||||||
eq(List.of("root->related")),
|
eq(List.of("root->related")),
|
||||||
argThat(profile -> profile != null
|
argThat(profile -> profile != null
|
||||||
@@ -113,10 +110,10 @@ class MemoryRecallProfileExtractorTest {
|
|||||||
MemorySlice slice = new MemorySlice(0, 2, "slice-summary");
|
MemorySlice slice = new MemorySlice(0, 2, "slice-summary");
|
||||||
unit.getSlices().add(slice);
|
unit.getSlices().add(slice);
|
||||||
|
|
||||||
updater.consume(new RollingResult(unit, slice, unit.getConversationMessages(), "slice-summary", 2, 6));
|
updater.consume(new RollingResult(unit.snapshot(), slice.snapshot(), 2, 6));
|
||||||
|
|
||||||
verify(memoryRuntime).recordMemory(
|
verify(memoryRuntime).recordMemory(
|
||||||
eq(unit),
|
eq(unit.snapshot()),
|
||||||
eq(null),
|
eq(null),
|
||||||
eq(List.of()),
|
eq(List.of()),
|
||||||
argThat(profile -> profile != null
|
argThat(profile -> profile != null
|
||||||
@@ -147,10 +144,10 @@ class MemoryRecallProfileExtractorTest {
|
|||||||
MemorySlice slice = new MemorySlice(0, 1, "slice-summary");
|
MemorySlice slice = new MemorySlice(0, 1, "slice-summary");
|
||||||
unit.getSlices().add(slice);
|
unit.getSlices().add(slice);
|
||||||
|
|
||||||
updater.consume(new RollingResult(unit, slice, unit.getConversationMessages(), "slice-summary", 1, 6));
|
updater.consume(new RollingResult(unit.snapshot(), slice.snapshot(), 1, 6));
|
||||||
|
|
||||||
verify(memoryRuntime).recordMemory(
|
verify(memoryRuntime).recordMemory(
|
||||||
eq(unit),
|
eq(unit.snapshot()),
|
||||||
eq("root->branch"),
|
eq("root->branch"),
|
||||||
eq(List.of()),
|
eq(List.of()),
|
||||||
argThat(profile -> profile != null
|
argThat(profile -> profile != null
|
||||||
|
|||||||
@@ -199,8 +199,14 @@ Partner/
|
|||||||
- [行动系统](doc/action/action.md)
|
- [行动系统](doc/action/action.md)
|
||||||
- [记忆存储与组织](doc/memory/memory.md)
|
- [记忆存储与组织](doc/memory/memory.md)
|
||||||
|
|
||||||
|
### 设计草案与后续方向
|
||||||
|
|
||||||
|
- [初见模块](doc/design/first-encounter-module.md)
|
||||||
|
- [印象模块更新管线](doc/design/impression-update-observation-pipeline.md)
|
||||||
|
- [印象模块向量融合扩展](doc/design/impression-vector-fusion.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
暂未指定。
|
暂未选择开源许可证。当前仓库主要作为个人项目展示与学习研究记录,未经授权不建议复制、分发或商用。
|
||||||
|
|||||||
337
doc/design/impression-update-observation-pipeline.md
Normal file
337
doc/design/impression-update-observation-pipeline.md
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
# Impression Update Observation Pipeline
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
当前 `ImpressionUpdater` 已经接入 `AfterRolling`,并形成了第一版更新闭环:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RollingResult
|
||||||
|
-> ImpressionUpdateContext
|
||||||
|
-> ImpressionUpdatePlanner
|
||||||
|
-> ImpressionUpdatePlanValidator
|
||||||
|
-> ImpressionUpdatePlanApplier
|
||||||
|
```
|
||||||
|
|
||||||
|
这一版验证了 rolling 后自动更新长期印象的主链路是可行的:Planner 生成更新计划,Validator 做安全校验,Applier 只执行 `CONFIRMED` 计划并通过 `CognitionCapability` mutation API 落地。
|
||||||
|
|
||||||
|
但当前 Planner 直接输出最终 `ImpressionUpdatePlan`,这会让 LLM 过早承担稳定身份和写入决策:
|
||||||
|
|
||||||
|
* 它既要从 rolling 证据中判断“观察到了什么”;
|
||||||
|
* 又要判断“应该更新哪个 known entity / 是否创建新 entity”;
|
||||||
|
* 还要直接产出 mutation patch。
|
||||||
|
|
||||||
|
其中后两者更适合由代码侧的聚合、实体解析、上下文补充和校验流程处理,不应该完全交给最初的 observation planner。
|
||||||
|
|
||||||
|
## 核心边界
|
||||||
|
|
||||||
|
新的设计目标是将“观察”和“更新”拆开:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ObservationPlanner 负责从证据中抽取观察。
|
||||||
|
Aggregator 负责合并重复观察。
|
||||||
|
Resolver 负责将合并观察解析为针对具体 Entity 的更新计划。
|
||||||
|
Validator 负责安全确认。
|
||||||
|
Applier 负责落库。
|
||||||
|
```
|
||||||
|
|
||||||
|
也就是说,Observation 层不是“去掉 uuid 的 ImpressionUpdatePlan”。
|
||||||
|
|
||||||
|
Observation 层只表达:
|
||||||
|
|
||||||
|
```text
|
||||||
|
从 rolling 证据和 active entity 中,观察到了某个实体相关的信息。
|
||||||
|
```
|
||||||
|
|
||||||
|
它不表达:
|
||||||
|
|
||||||
|
```text
|
||||||
|
应该更新哪个长期实体;
|
||||||
|
应该创建哪个实体;
|
||||||
|
应该写入哪些 mutation patch。
|
||||||
|
```
|
||||||
|
|
||||||
|
`ImpressionPatch`、`FeaturePatch`、`RelationPatch`、`UpdateExistingStep`、`CreateEntityStep` 只应出现在 Resolver 之后产生的 `ImpressionUpdatePlan` 中。
|
||||||
|
|
||||||
|
## 目标流程
|
||||||
|
|
||||||
|
目标管线如下:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RollingResult + ActiveEntityBatch
|
||||||
|
-> ImpressionObservationPlanner
|
||||||
|
-> ImpressionEntityObservation collection
|
||||||
|
|
||||||
|
All Observations + KnownEntityIdentity index
|
||||||
|
-> ImpressionObservationAggregator
|
||||||
|
-> Merged ImpressionEntityObservation collection
|
||||||
|
|
||||||
|
Merged Observations + related Entity snapshots
|
||||||
|
-> ImpressionObservationResolver
|
||||||
|
-> ImpressionUpdatePlan fragments
|
||||||
|
|
||||||
|
All ImpressionUpdatePlan fragments
|
||||||
|
-> ImpressionUpdatePlanValidator
|
||||||
|
-> ImpressionUpdatePlanApplier
|
||||||
|
```
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
* `ImpressionObservationPlanner` 只从 rolling evidence 和 active entity batch 中抽取观察;
|
||||||
|
* `ImpressionObservationAggregator` 结合轻量 known identity index 合并重复观察、归并 evidence、过滤弱观察;
|
||||||
|
* `ImpressionObservationResolver` 接收合并后的观察与相关完整 Entity snapshot,产出针对该 Entity 的 `ImpressionUpdatePlan` fragment;
|
||||||
|
* `ImpressionUpdatePlanValidator` 对所有 fragments 合并后的最终计划做全局校验;
|
||||||
|
* `ImpressionUpdatePlanApplier` 继续复用现有落库逻辑。
|
||||||
|
|
||||||
|
## Observation 模型
|
||||||
|
|
||||||
|
第一阶段只需要两个核心模型。
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class ImpressionEntityObservation @JvmOverloads constructor(
|
||||||
|
val proposedSubject: String,
|
||||||
|
val aliases: List<String> = emptyList(),
|
||||||
|
val impressions: Map<String, Int> = emptyMap(),
|
||||||
|
val features: Map<String, Int> = emptyMap(),
|
||||||
|
val relations: Map<String, Map<String, Int>> = emptyMap(),
|
||||||
|
val sourceActiveRuntimeIds: List<String> = emptyList(),
|
||||||
|
val evidenceSnippets: List<String> = emptyList(),
|
||||||
|
val reason: String? = null,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
字段语义:
|
||||||
|
|
||||||
|
* `proposedSubject`:观察层实体名,不是最终 canonical subject;
|
||||||
|
* `aliases`:证据中出现的别名或称呼;
|
||||||
|
* `impressions`:观察到的长期印象文本,`Int` 表示观察权重或聚合计数;
|
||||||
|
* `features`:观察到的特征文本,`Int` 表示观察权重或聚合计数;
|
||||||
|
* `relations`:`target -> relation text -> weight/count`;
|
||||||
|
* `sourceActiveRuntimeIds`:观察来自哪些 active runtime entity;
|
||||||
|
* `evidenceSnippets`:支持该观察的证据片段;
|
||||||
|
* `reason`:Planner 或聚合阶段给出的简短理由,仅用于审计,不作为执行许可。
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class KnownEntityIdentity @JvmOverloads constructor(
|
||||||
|
val entityUuid: String,
|
||||||
|
val subject: String,
|
||||||
|
val aliases: List<String> = emptyList(),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`KnownEntityIdentity` 是轻量身份索引,只包含稳定身份字段,不携带 impressions、features、relations 等完整语义内容。
|
||||||
|
|
||||||
|
它主要用于 Aggregator 判断不同 observation 是否可能指向同一 known entity,避免仅靠 `proposedSubject` 做粗糙合并。
|
||||||
|
|
||||||
|
真正生成 patch 时,需要在聚合完成后取得相关完整 Entity snapshot,再由 Resolver 结合 Entity 当前状态制定更新计划。
|
||||||
|
|
||||||
|
## Batch 语义
|
||||||
|
|
||||||
|
Active entities 可以按上下文预算分批交给 ObservationPlanner:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RollingResult + ActiveEntityBatch
|
||||||
|
-> ImpressionEntityObservation collection
|
||||||
|
```
|
||||||
|
|
||||||
|
这里的 batch size 只是上下文预算,不是语义覆盖上限。
|
||||||
|
|
||||||
|
所有 batch 的 observations 必须先聚合,再进入 Resolver 阶段。
|
||||||
|
|
||||||
|
不允许:
|
||||||
|
|
||||||
|
```text
|
||||||
|
batch observation -> batch update plan -> batch apply
|
||||||
|
```
|
||||||
|
|
||||||
|
因为这样会让更新顺序影响长期状态,并且难以做全局冲突校验。
|
||||||
|
|
||||||
|
允许:
|
||||||
|
|
||||||
|
```text
|
||||||
|
observation batch 1 -> observations
|
||||||
|
observation batch 2 -> observations
|
||||||
|
observation batch 3 -> observations
|
||||||
|
|
||||||
|
all observations -> aggregate -> resolve -> plan fragments
|
||||||
|
all plan fragments -> global validation -> apply
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolver 阶段如果上下文较大,也可以按合并后的 observation / related entity 分批产出 `ImpressionUpdatePlan` fragments。
|
||||||
|
|
||||||
|
但所有 fragments 仍应统一汇总后再由 Validator 校验和确认,不应分批直接落库。
|
||||||
|
|
||||||
|
## Aggregator
|
||||||
|
|
||||||
|
Aggregator 的输入是所有 ObservationPlanner 产出的 `ImpressionEntityObservation`,以及轻量 `KnownEntityIdentity` index。
|
||||||
|
|
||||||
|
Aggregator 负责:
|
||||||
|
|
||||||
|
* 丢弃空 observation;
|
||||||
|
* normalize subject / alias;
|
||||||
|
* 合并明显重复的 observation;
|
||||||
|
* 合并同一 active runtime entity 来源的重复观察;
|
||||||
|
* 借助 known subject / alias 合并指向同一 known entity 的观察;
|
||||||
|
* 合并 impression / feature / relation 的计数;
|
||||||
|
* 合并 evidence snippets 与 source active runtime ids;
|
||||||
|
* 过滤无证据、过弱、模板化或过长的观察。
|
||||||
|
|
||||||
|
Aggregator 不生成 `ImpressionUpdatePlan`,也不落库。
|
||||||
|
|
||||||
|
它的输出仍然是合并后的 observation collection。
|
||||||
|
|
||||||
|
## Resolver
|
||||||
|
|
||||||
|
Resolver 的输入是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Merged ImpressionEntityObservation
|
||||||
|
+ related Entity snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolver 的职责是将合并后的观察解析成针对具体 Entity 的更新计划。
|
||||||
|
|
||||||
|
它可以根据完整 Entity 当前状态判断:
|
||||||
|
|
||||||
|
* 观察到的 impression 是否已经存在;
|
||||||
|
* 观察到的 feature 是否重复或需要更新;
|
||||||
|
* 观察到的 relation target 是否能解析;
|
||||||
|
* 观察是否足够强,可以创建新 entity;
|
||||||
|
* 观察是否 ambiguous,需要 reject / postpone;
|
||||||
|
* 观察是否只适合保留为 evidence,不进入 mutation plan。
|
||||||
|
|
||||||
|
Resolver 输出的是 `ImpressionUpdatePlan` fragment,状态应保持为 `PREPARED`。
|
||||||
|
|
||||||
|
Resolver 不直接确认计划,不直接落库。
|
||||||
|
|
||||||
|
## UpdatePlan 复用
|
||||||
|
|
||||||
|
新管线继续复用现有 mutation plan:
|
||||||
|
|
||||||
|
* `ImpressionUpdatePlan`
|
||||||
|
* `UpdateExistingStep`
|
||||||
|
* `CreateEntityStep`
|
||||||
|
* `ImpressionPatch`
|
||||||
|
* `FeaturePatch`
|
||||||
|
* `AliasPatch`
|
||||||
|
* `SubjectPatch`
|
||||||
|
* `RelationPatch`
|
||||||
|
* `ImpressionUpdatePlanValidator`
|
||||||
|
* `ImpressionUpdatePlanApplier`
|
||||||
|
|
||||||
|
也就是说,新管线不推翻现有 `ImpressionUpdatePlan`,而是在它前面增加 observation / aggregation / resolution 层。
|
||||||
|
|
||||||
|
旧链路的问题不是 `ImpressionUpdatePlan` 本身,而是 Planner 太早产出了它。
|
||||||
|
|
||||||
|
新的边界是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ObservationPlanner 不产 UpdatePlan。
|
||||||
|
Resolver 才产 UpdatePlan。
|
||||||
|
Validator 才确认 UpdatePlan。
|
||||||
|
Applier 才执行 UpdatePlan。
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validator
|
||||||
|
|
||||||
|
Validator 从基础结构校验升级为 context-aware 校验。
|
||||||
|
|
||||||
|
它接收所有 Resolver 产出的 plan fragments 汇总后的最终 `ImpressionUpdatePlan`,并做全局校验:
|
||||||
|
|
||||||
|
* `UpdateExistingStep.entityUuid` 必须存在;
|
||||||
|
* `CreateEntityStep.subject` 不应与 known subject / alias 冲突;
|
||||||
|
* relation target 必须能解析到已知实体或本批新建实体;
|
||||||
|
* patch 文本必须非空且长度有限;
|
||||||
|
* ambiguous / postponed observation 不允许进入落库计划;
|
||||||
|
* final plan 不允许空 step;
|
||||||
|
* LLM / Resolver 只能产 `PREPARED`,不能直接产 `CONFIRMED`。
|
||||||
|
|
||||||
|
Validator 通过后,由代码侧构造 `CONFIRMED` plan,再交给 Applier。
|
||||||
|
|
||||||
|
## Applier
|
||||||
|
|
||||||
|
Applier 继续只执行 `CONFIRMED` 的 `ImpressionUpdatePlan`。
|
||||||
|
|
||||||
|
落库仍通过 `CognitionCapability` mutation API 执行,不绕过 `ImpressionCore`。
|
||||||
|
|
||||||
|
如果最终计划较大,可以在 Applier 内部按资源或事务边界分批执行;但这只是执行层 batch,不应影响前面的 observation、aggregation、resolution 和 validation 语义。
|
||||||
|
|
||||||
|
## 分阶段落地
|
||||||
|
|
||||||
|
### Phase 1:观察模型
|
||||||
|
|
||||||
|
* 新增 `ImpressionEntityObservation`;
|
||||||
|
* 新增 `KnownEntityIdentity`;
|
||||||
|
* 保留现有 `ImpressionUpdatePlan` / Patch / Applier;
|
||||||
|
* 不改主链路。
|
||||||
|
|
||||||
|
### Phase 2:ObservationPlanner
|
||||||
|
|
||||||
|
* 新增 `ImpressionObservationPlanner`;
|
||||||
|
* 输入为 `RollingResult + ActiveEntityBatch`;
|
||||||
|
* 输出 `ImpressionEntityObservation` collection;
|
||||||
|
* Planner 不输出 known entity uuid;
|
||||||
|
* Planner 不决定 update/create;
|
||||||
|
* Planner 不输出 mutation patch。
|
||||||
|
|
||||||
|
### Phase 3:Aggregator
|
||||||
|
|
||||||
|
* 聚合所有 batch observations;
|
||||||
|
* 使用 `KnownEntityIdentity` 辅助 subject / alias 归并;
|
||||||
|
* 输出合并后的 observation collection;
|
||||||
|
* 不生成 `ImpressionUpdatePlan`。
|
||||||
|
|
||||||
|
### Phase 4:Resolver
|
||||||
|
|
||||||
|
* 根据合并后的 observation 找到相关 Entity snapshot;
|
||||||
|
* 结合 Entity 当前状态生成 `ImpressionUpdatePlan` fragments;
|
||||||
|
* fragment 状态保持 `PREPARED`;
|
||||||
|
* 不确认、不落库。
|
||||||
|
|
||||||
|
### Phase 5:Validator 升级
|
||||||
|
|
||||||
|
* 汇总所有 plan fragments;
|
||||||
|
* 做 context-aware / entity-aware / relation-aware 校验;
|
||||||
|
* 校验通过后由代码侧构造 `CONFIRMED` final plan。
|
||||||
|
|
||||||
|
### Phase 6:替换主流程
|
||||||
|
|
||||||
|
最终将 `ImpressionUpdater.consume()` 改为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
buildObservationContexts(result)
|
||||||
|
-> planner per active entity batch
|
||||||
|
-> aggregate observations
|
||||||
|
-> load related entity snapshots
|
||||||
|
-> resolve to update plan fragments
|
||||||
|
-> merge fragments
|
||||||
|
-> validate
|
||||||
|
-> confirm
|
||||||
|
-> apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
第一版不做:
|
||||||
|
|
||||||
|
* 向量召回;
|
||||||
|
* 全库 impressions / features 语义扫描;
|
||||||
|
* LLM 实体合并;
|
||||||
|
* 多实体复杂冲突解决;
|
||||||
|
* 自动删除或降权旧 impression;
|
||||||
|
* 基于弱证据的大规模新实体创建;
|
||||||
|
* batch 级直接 apply。
|
||||||
|
|
||||||
|
这些应在 observation pipeline 稳定后单独设计。
|
||||||
|
|
||||||
|
## 当前结论
|
||||||
|
|
||||||
|
本设计的核心是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Observation 不是 UpdatePlan。
|
||||||
|
Observation 只记录证据化观察。
|
||||||
|
UpdatePlan 在合并观察并取得相关 Entity 后生成。
|
||||||
|
Applier 只执行经过 Validator 确认的最终计划。
|
||||||
|
```
|
||||||
|
|
||||||
|
这样可以保留 LLM 对自然语言证据的抽取能力,同时避免让最初的 Planner 直接承担长期实体身份和数据库 mutation 决策。
|
||||||
Reference in New Issue
Block a user