From 371b4a01d7819f5afe26fc06510b0c4dc6faa8f3 Mon Sep 17 00:00:00 2001 From: slhafzjw Date: Wed, 10 Jun 2026 14:02:31 +0800 Subject: [PATCH] feat(impression): add core mutation APIs Expose core-owned entity creation, binding, and impression update methods so updater logic can request mutations without bypassing indexes. Add ActiveEntity snapshots for safe inspection and keep test stubs aligned with CognitionCapability. --- .../core/cognition/CognitionCapability.java | 41 ++++ .../core/cognition/impression/ActiveEntity.kt | 17 ++ .../cognition/impression/ImpressionCore.java | 187 ++++++++++++++++++ .../module/impression/ImpressionUpdater.java | 19 ++ .../CommunicationProducerTest.java | 43 ++++ .../memory/runtime/MemoryRuntimeTest.java | 43 ++++ 6 files changed, 350 insertions(+) create mode 100644 Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdater.java diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/CognitionCapability.java b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/CognitionCapability.java index 30bc238b..4f01ffc4 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/CognitionCapability.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/CognitionCapability.java @@ -3,10 +3,12 @@ package work.slhaf.partner.core.cognition; import org.w3c.dom.Element; 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.framework.agent.factory.capability.annotation.Capability; import work.slhaf.partner.framework.agent.model.pojo.Message; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; @@ -29,6 +31,45 @@ public interface CognitionCapability { Lock getMessageLock(); + /** + * Project user input onto known or currently active entities and append the input as runtime evidence. + */ Set 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 showEntities(); + + /** + * Create and register a new known entity by subject, then refresh search indexes for it. + */ + Entity createEntity(String subject); + + /** + * Return a known entity by uuid, or null when it does not exist. + */ + Entity getEntity(String uuid); + + /** + * Bind an active runtime entity to a known entity and refresh the active-entity search document. + */ + boolean bindActiveEntity(String runtimeId, String entityUuid); + + /** + * 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); + } diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ActiveEntity.kt b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ActiveEntity.kt index 9d3f31fa..bab3ae24 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ActiveEntity.kt +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ActiveEntity.kt @@ -64,6 +64,23 @@ class ActiveEntity @JvmOverloads constructor( 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) { root.setAttribute("runtime_id", runtimeId) boundEntityUuid?.let { root.setAttribute("bound_entity_uuid", it) } diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ImpressionCore.java b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ImpressionCore.java index 27039144..fcd6c946 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ImpressionCore.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ImpressionCore.java @@ -69,6 +69,170 @@ public class ImpressionCore implements StateSerializable { return projected; } + /** + * 列出当前已存在的 ActiveEntity 以及对应的 Entity。ActiveEntity 返回快照,Entity 返回当前已知实体引用。 + * + * 注意:外部模块不要直接修改返回的 Entity,否则文本索引 / 向量索引不会刷新。 + * Impression 更新应走 updateEntity* 系列接口。 + * + * @return ActiveEntity 快照与已绑定 Entity 的映射 + */ + @CapabilityMethod + public Map showEntities() { + Map result = new LinkedHashMap<>(); + List 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 Entity 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; + } + + /** + * 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); + } + + /** + * 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 = findActiveEntityByRuntimeId(runtimeId); + if (activeEntity.isEmpty()) { + return false; + } + + ActiveEntity active = activeEntity.get(); + active.bindEntity(entityUuid); + active.updateSubject(entity.getSubject()); + refreshActiveEntityTextSearch(active); + 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 aggregateMatches( List hits, int limit @@ -183,6 +347,28 @@ 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); + } + + /** + * 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() { List documents = new ArrayList<>(); knownEntitiesByUuid.values().forEach(entity -> @@ -222,6 +408,7 @@ public class ImpressionCore implements StateSerializable { } Entity entity = new Entity(uuid, subject); + entity.register(); entity.load(); vectorIndex.sync(entity); knownEntitiesByUuid.put(uuid, entity); diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdater.java b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdater.java new file mode 100644 index 00000000..68af7652 --- /dev/null +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdater.java @@ -0,0 +1,19 @@ +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.module.communication.AfterRolling; +import work.slhaf.partner.module.communication.RollingResult; + +public class ImpressionUpdater extends AbstractAgentModule.Standalone implements AfterRolling { + + @InjectCapability + private CognitionCapability cognitionCapability; + + @Override + public void consume(RollingResult result) { + + } + +} diff --git a/Partner-Core/src/test/java/work/slhaf/partner/module/communication/CommunicationProducerTest.java b/Partner-Core/src/test/java/work/slhaf/partner/module/communication/CommunicationProducerTest.java index 7cbe12bd..0ec5307b 100644 --- a/Partner-Core/src/test/java/work/slhaf/partner/module/communication/CommunicationProducerTest.java +++ b/Partner-Core/src/test/java/work/slhaf/partner/module/communication/CommunicationProducerTest.java @@ -145,5 +145,48 @@ class CommunicationProducerTest { public Lock getMessageLock() { return lock; } + + @Override + public java.util.Set 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 work.slhaf.partner.core.cognition.impression.Entity createEntity(String subject) { + return null; + } + + @Override + public work.slhaf.partner.core.cognition.impression.Entity getEntity(String uuid) { + return null; + } + + @Override + public boolean bindActiveEntity(String runtimeId, String entityUuid) { + 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; + } } } diff --git a/Partner-Core/src/test/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeTest.java b/Partner-Core/src/test/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeTest.java index a14e37a3..778740d2 100644 --- a/Partner-Core/src/test/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeTest.java +++ b/Partner-Core/src/test/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeTest.java @@ -98,6 +98,49 @@ class MemoryRuntimeTest { public Lock getMessageLock() { return lock; } + + @Override + public java.util.Set 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 work.slhaf.partner.core.cognition.impression.Entity createEntity(String subject) { + return null; + } + + @Override + public work.slhaf.partner.core.cognition.impression.Entity getEntity(String uuid) { + return null; + } + + @Override + public boolean bindActiveEntity(String runtimeId, String entityUuid) { + 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; + } }; }