mirror of
https://github.com/slhaf/Partner.git
synced 2026-10-25 02:43:49 +00:00
Compare commits
5
Commits
6a64ff29c4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a45cb0a6c8 | ||
|
|
b414a5eb8e | ||
|
|
bc2c993473 | ||
|
|
0e693310f8 | ||
|
|
82a33c3909 |
+6
-1
@@ -45,13 +45,18 @@ public interface CognitionCapability {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
+14
-4
@@ -103,7 +103,7 @@ public class ImpressionCore implements StateSerializable {
|
||||
* Create a new known entity and make it visible to recall/update indexes immediately.
|
||||
*/
|
||||
@CapabilityMethod
|
||||
public Entity createEntity(String subject) {
|
||||
public String createEntity(String subject) {
|
||||
if (subject == null || subject.isBlank()) {
|
||||
throw new IllegalArgumentException("subject must not be blank");
|
||||
}
|
||||
@@ -112,7 +112,7 @@ public class ImpressionCore implements StateSerializable {
|
||||
entity.register();
|
||||
knownEntitiesByUuid.put(entity.getUuid(), entity);
|
||||
refreshKnownEntityIndexes(entity);
|
||||
return entity;
|
||||
return entity.getUuid();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,6 +126,16 @@ public class ImpressionCore implements StateSerializable {
|
||||
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.
|
||||
@@ -328,7 +338,7 @@ public class ImpressionCore implements StateSerializable {
|
||||
private Optional<ActiveEntity> resolveActiveEntity(ImpressionSearchTarget target) {
|
||||
return switch (target.getType()) {
|
||||
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);
|
||||
if (knownEntity == null) {
|
||||
return Optional.empty();
|
||||
|
||||
+18
@@ -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(),
|
||||
)
|
||||
+16
@@ -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());
|
||||
}
|
||||
}
|
||||
+19
@@ -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
|
||||
) {
|
||||
}
|
||||
+96
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+71
@@ -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();
|
||||
}
|
||||
}
|
||||
+78
@@ -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));
|
||||
}
|
||||
}
|
||||
+92
@@ -1,19 +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
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-1
@@ -160,7 +160,7 @@ class CommunicationProducerTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public work.slhaf.partner.core.cognition.impression.Entity createEntity(String subject) {
|
||||
public String createEntity(String subject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -169,6 +169,11 @@ class CommunicationProducerTest {
|
||||
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;
|
||||
|
||||
+323
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -111,7 +111,7 @@ class MemoryRuntimeTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public work.slhaf.partner.core.cognition.impression.Entity createEntity(String subject) {
|
||||
public String createEntity(String subject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -120,6 +120,11 @@ class MemoryRuntimeTest {
|
||||
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;
|
||||
|
||||
@@ -199,8 +199,14 @@ Partner/
|
||||
- [行动系统](doc/action/action.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
|
||||
|
||||
暂未选择开源许可证。当前仓库主要作为个人项目展示与学习研究记录,未经授权不建议复制、分发或商用。
|
||||
暂未选择开源许可证。当前仓库主要作为个人项目展示与学习研究记录,未经授权不建议复制、分发或商用。
|
||||
|
||||
@@ -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