feat(impression): add after-rolling updater pipeline

This commit is contained in:
2026-06-19 22:51:33 +08:00
parent 6a64ff29c4
commit 82a33c3909
11 changed files with 727 additions and 7 deletions

View File

@@ -45,13 +45,18 @@ public interface CognitionCapability {
/** /**
* Create and register a new known entity by subject, then refresh search indexes for it. * Create and register a new known entity by subject, then refresh search indexes for it.
*/ */
Entity createEntity(String subject); String createEntity(String subject);
/** /**
* Return a known entity by uuid, or null when it does not exist. * Return a known entity by uuid, or null when it does not exist.
*/ */
Entity getEntity(String uuid); 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. * Bind an active runtime entity to a known entity and refresh the active-entity search document.
*/ */

View File

@@ -103,7 +103,7 @@ public class ImpressionCore implements StateSerializable {
* Create a new known entity and make it visible to recall/update indexes immediately. * Create a new known entity and make it visible to recall/update indexes immediately.
*/ */
@CapabilityMethod @CapabilityMethod
public Entity createEntity(String subject) { public String createEntity(String subject) {
if (subject == null || subject.isBlank()) { if (subject == null || subject.isBlank()) {
throw new IllegalArgumentException("subject must not be blank"); throw new IllegalArgumentException("subject must not be blank");
} }
@@ -112,7 +112,7 @@ public class ImpressionCore implements StateSerializable {
entity.register(); entity.register();
knownEntitiesByUuid.put(entity.getUuid(), entity); knownEntitiesByUuid.put(entity.getUuid(), entity);
refreshKnownEntityIndexes(entity); refreshKnownEntityIndexes(entity);
return entity; return entity.getUuid();
} }
/** /**
@@ -126,6 +126,16 @@ public class ImpressionCore implements StateSerializable {
return knownEntitiesByUuid.get(uuid); 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. * 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. * This keeps the active entity in current context while giving later updates a stable storage target.
@@ -328,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());
}; };
} }
@@ -348,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();

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 字段必须使用简洁、稳定、可索引的表达。
- 不要输出 CONFIRMEDCONFIRMED 只能由代码 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));
}
}

View File

@@ -1,19 +1,111 @@
package work.slhaf.partner.module.impression; 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.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.capability.annotation.InjectCapability;
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule; 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.AfterRolling;
import work.slhaf.partner.module.communication.AfterRollingRegistry;
import work.slhaf.partner.module.communication.RollingResult; import work.slhaf.partner.module.communication.RollingResult;
import java.util.List;
import java.util.Set;
public class ImpressionUpdater extends AbstractAgentModule.Standalone implements AfterRolling { public class ImpressionUpdater extends AbstractAgentModule.Standalone implements AfterRolling {
@InjectCapability @InjectCapability
private CognitionCapability cognitionCapability; 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 @Override
public void consume(RollingResult result) { 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
));
} }
} }

View File

@@ -160,7 +160,7 @@ class CommunicationProducerTest {
} }
@Override @Override
public work.slhaf.partner.core.cognition.impression.Entity createEntity(String subject) { public String createEntity(String subject) {
return null; return null;
} }
@@ -169,6 +169,11 @@ class CommunicationProducerTest {
return null; return null;
} }
@Override
public work.slhaf.partner.core.cognition.impression.ActiveEntity activateKnownEntity(String entityUuid) {
return null;
}
@Override @Override
public boolean bindActiveEntity(String runtimeId, String entityUuid) { public boolean bindActiveEntity(String runtimeId, String entityUuid) {
return false; return false;

View File

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

View File

@@ -111,7 +111,7 @@ class MemoryRuntimeTest {
} }
@Override @Override
public work.slhaf.partner.core.cognition.impression.Entity createEntity(String subject) { public String createEntity(String subject) {
return null; return null;
} }
@@ -120,6 +120,11 @@ class MemoryRuntimeTest {
return null; return null;
} }
@Override
public work.slhaf.partner.core.cognition.impression.ActiveEntity activateKnownEntity(String entityUuid) {
return null;
}
@Override @Override
public boolean bindActiveEntity(String runtimeId, String entityUuid) { public boolean bindActiveEntity(String runtimeId, String entityUuid) {
return false; return false;