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.
This commit is contained in:
2026-06-10 14:02:31 +08:00
parent 0567837dfe
commit 371b4a01d7
6 changed files with 350 additions and 0 deletions

View File

@@ -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,45 @@ 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.
*/
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);
} }

View File

@@ -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) }

View File

@@ -69,6 +69,170 @@ 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 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> 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<EntityAssociationMatch> aggregateMatches( private List<EntityAssociationMatch> aggregateMatches(
List<ImpressionSearchHit> hits, List<ImpressionSearchHit> hits,
int limit 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() { private void rebuildTextSearch() {
List<ImpressionSearchDocument> documents = new ArrayList<>(); List<ImpressionSearchDocument> documents = new ArrayList<>();
knownEntitiesByUuid.values().forEach(entity -> knownEntitiesByUuid.values().forEach(entity ->
@@ -222,6 +408,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);

View File

@@ -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) {
}
}

View File

@@ -145,5 +145,48 @@ 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 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;
}
} }
} }

View File

@@ -98,6 +98,49 @@ 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 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;
}
}; };
} }