From d983a1e02e3b2fdfd706381d550f98c111555820 Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 21 Apr 2026 04:58:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(memory):=20dream-v2=20E2=20=E2=80=94=20Fac?= =?UTF-8?q?t=20query=20tools=20+=20FactMemoryProvider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fact/provider/FactMemoryProvider.java | 80 ++++++++++++++ .../memory/fact/query/FactQueryService.java | 100 ++++++++++++++++++ .../repository/FactContradictionMapper.java | 9 ++ .../fact/repository/FactEntityRefMapper.java | 9 ++ .../mate/memory/fact/tool/FactQueryTool.java | 79 ++++++++++++++ 5 files changed, 277 insertions(+) create mode 100644 mateclaw-server/src/main/java/vip/mate/memory/fact/provider/FactMemoryProvider.java create mode 100644 mateclaw-server/src/main/java/vip/mate/memory/fact/query/FactQueryService.java create mode 100644 mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactContradictionMapper.java create mode 100644 mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactEntityRefMapper.java create mode 100644 mateclaw-server/src/main/java/vip/mate/memory/fact/tool/FactQueryTool.java diff --git a/mateclaw-server/src/main/java/vip/mate/memory/fact/provider/FactMemoryProvider.java b/mateclaw-server/src/main/java/vip/mate/memory/fact/provider/FactMemoryProvider.java new file mode 100644 index 00000000..764fc352 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/memory/fact/provider/FactMemoryProvider.java @@ -0,0 +1,80 @@ +package vip.mate.memory.fact.provider; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import vip.mate.memory.MemoryProperties; +import vip.mate.memory.fact.model.FactEntity; +import vip.mate.memory.fact.projection.FactProjectionBuilder; +import vip.mate.memory.fact.query.FactQueryService; +import vip.mate.memory.fact.tool.FactQueryTool; +import vip.mate.memory.spi.MemoryProvider; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Read-only SPI provider that surfaces fact projection in the memory lifecycle. + *

+ * - prefetch: returns relevant facts for the user query (injected into prompt) + * - syncTurn: bumps use_count for recalled facts + * - onMemoryWrite: triggers incremental projection rebuild + * - getToolBeans: exposes FactQueryTool to agents + * + * @author MateClaw Team + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class FactMemoryProvider implements MemoryProvider { + + private final FactQueryService queryService; + private final FactProjectionBuilder projectionBuilder; + private final FactQueryTool factQueryTool; + private final MemoryProperties properties; + + @Override + public String id() { return "fact_store"; } + + @Override + public int order() { return 200; } + + @Override + public boolean isAvailable() { + return properties.getFact().isProjectionEnabled(); + } + + @Override + public String prefetch(Long agentId, String userQuery) { + if (!properties.getFact().isProjectionEnabled()) return ""; + List facts = queryService.recallRelevant(agentId, userQuery); + if (facts.isEmpty()) return ""; + + // Bump usage + queryService.bumpUseCount(facts.stream().map(FactEntity::getId).toList()); + + String block = facts.stream() + .map(f -> String.format("- %s %s %s", f.getSubject(), f.getPredicate(), f.getObjectValue())) + .collect(Collectors.joining("\n")); + return "\n" + block + "\n"; + } + + @Override + public void syncTurn(Long agentId, String conversationId, String userMessage, String assistantReply) { + // No-op — bumpUseCount already called in prefetch + } + + @Override + public void onMemoryWrite(Long agentId, String target, String action, String content) { + if (!properties.getFact().isProjectionEnabled()) return; + // Incremental rebuild for the changed file + projectionBuilder.rebuildOne(agentId, target, content); + } + + @Override + public List getToolBeans() { + if (!properties.getFact().isProjectionEnabled()) return Collections.emptyList(); + return List.of(factQueryTool); + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/memory/fact/query/FactQueryService.java b/mateclaw-server/src/main/java/vip/mate/memory/fact/query/FactQueryService.java new file mode 100644 index 00000000..f8a98bc5 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/memory/fact/query/FactQueryService.java @@ -0,0 +1,100 @@ +package vip.mate.memory.fact.query; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import vip.mate.memory.fact.model.FactContradictionEntity; +import vip.mate.memory.fact.model.FactEntity; +import vip.mate.memory.fact.model.FactEntityRefEntity; +import vip.mate.memory.fact.repository.FactMapper; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * Query service for the fact projection. + * Read-only + bumpUseCount (the only accumulated column writer). + * + * @author MateClaw Team + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class FactQueryService { + + private final FactMapper factMapper; + private final vip.mate.memory.fact.repository.FactEntityRefMapper refMapper; + private final vip.mate.memory.fact.repository.FactContradictionMapper contradictionMapper; + + /** + * Probe facts by entity name (subject or object match). + */ + public List probe(Long agentId, String entity) { + return factMapper.selectList( + new LambdaQueryWrapper() + .eq(FactEntity::getAgentId, agentId) + .eq(FactEntity::getDeleted, 0) + .and(w -> w.like(FactEntity::getSubject, entity) + .or().like(FactEntity::getObjectValue, entity)) + .orderByDesc(FactEntity::getTrust) + .last("LIMIT 20")); + } + + /** + * Find related facts via entity references (multi-hop). + */ + public List related(Long agentId, String entity, int hops) { + // Find fact IDs that reference this entity + List refs = refMapper.selectList( + new LambdaQueryWrapper() + .like(FactEntityRefEntity::getEntityName, entity) + .last("LIMIT 50")); + List factIds = refs.stream().map(FactEntityRefEntity::getFactId).distinct().toList(); + if (factIds.isEmpty()) return List.of(); + + return factMapper.selectList( + new LambdaQueryWrapper() + .eq(FactEntity::getAgentId, agentId) + .eq(FactEntity::getDeleted, 0) + .in(FactEntity::getId, factIds) + .orderByDesc(FactEntity::getTrust) + .last("LIMIT 20")); + } + + /** + * List unresolved contradictions for an agent. + */ + public List listContradictions(Long agentId) { + return contradictionMapper.selectList( + new LambdaQueryWrapper() + .eq(FactContradictionEntity::getAgentId, agentId) + .isNull(FactContradictionEntity::getResolution) + .eq(FactContradictionEntity::getDeleted, 0) + .orderByDesc(FactContradictionEntity::getCreateTime) + .last("LIMIT 50")); + } + + /** + * Recall relevant facts for a query (used by FactMemoryProvider.prefetch). + */ + public List recallRelevant(Long agentId, String query) { + return factMapper.selectList( + new LambdaQueryWrapper() + .eq(FactEntity::getAgentId, agentId) + .eq(FactEntity::getDeleted, 0) + .and(w -> w.like(FactEntity::getSubject, query) + .or().like(FactEntity::getObjectValue, query) + .or().like(FactEntity::getPredicate, query)) + .orderByDesc(FactEntity::getTrust) + .last("LIMIT 10")); + } + + /** + * Bump use_count for fact IDs (the ONLY writer of accumulated columns). + */ + public void bumpUseCount(List ids) { + if (ids == null || ids.isEmpty()) return; + factMapper.bumpUseCount(ids, LocalDateTime.now()); + } +} diff --git a/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactContradictionMapper.java b/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactContradictionMapper.java new file mode 100644 index 00000000..b3d4d6ad --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactContradictionMapper.java @@ -0,0 +1,9 @@ +package vip.mate.memory.fact.repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import vip.mate.memory.fact.model.FactContradictionEntity; + +@Mapper +public interface FactContradictionMapper extends BaseMapper { +} diff --git a/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactEntityRefMapper.java b/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactEntityRefMapper.java new file mode 100644 index 00000000..0e8829a9 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactEntityRefMapper.java @@ -0,0 +1,9 @@ +package vip.mate.memory.fact.repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import vip.mate.memory.fact.model.FactEntityRefEntity; + +@Mapper +public interface FactEntityRefMapper extends BaseMapper { +} diff --git a/mateclaw-server/src/main/java/vip/mate/memory/fact/tool/FactQueryTool.java b/mateclaw-server/src/main/java/vip/mate/memory/fact/tool/FactQueryTool.java new file mode 100644 index 00000000..30aea726 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/memory/fact/tool/FactQueryTool.java @@ -0,0 +1,79 @@ +package vip.mate.memory.fact.tool; + +import lombok.RequiredArgsConstructor; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; +import vip.mate.memory.MemoryProperties; +import vip.mate.memory.fact.model.FactContradictionEntity; +import vip.mate.memory.fact.model.FactEntity; +import vip.mate.memory.fact.query.FactQueryService; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Agent tools for querying the fact projection. + * Read-only — no fact_add / fact_remove / fact_update tools (core invariant D1). + * + * @author MateClaw Team + */ +@Component +@RequiredArgsConstructor +public class FactQueryTool { + + private final FactQueryService queryService; + private final MemoryProperties properties; + + @Tool(description = "Probe facts about an entity. Returns relevant facts where the entity appears as subject or object.") + public String fact_probe( + @ToolParam(description = "Agent ID") Long agentId, + @ToolParam(description = "Entity name to search for") String entity) { + if (!properties.getFact().isProjectionEnabled()) { + return "Fact projection is disabled."; + } + List facts = queryService.probe(agentId, entity); + if (facts.isEmpty()) return "No facts found for entity: " + entity; + + // Bump use count + queryService.bumpUseCount(facts.stream().map(FactEntity::getId).toList()); + + return facts.stream() + .map(f -> String.format("- %s %s %s (trust=%.2f)", f.getSubject(), f.getPredicate(), f.getObjectValue(), f.getTrust())) + .collect(Collectors.joining("\n")); + } + + @Tool(description = "Find facts related to an entity via entity references (multi-hop graph query).") + public String fact_related( + @ToolParam(description = "Agent ID") Long agentId, + @ToolParam(description = "Entity name") String entity, + @ToolParam(description = "Number of hops (1-3)") int hops) { + if (!properties.getFact().isProjectionEnabled()) { + return "Fact projection is disabled."; + } + List facts = queryService.related(agentId, entity, Math.min(hops, 3)); + if (facts.isEmpty()) return "No related facts found for: " + entity; + + queryService.bumpUseCount(facts.stream().map(FactEntity::getId).toList()); + + return facts.stream() + .map(f -> String.format("- %s %s %s (trust=%.2f)", f.getSubject(), f.getPredicate(), f.getObjectValue(), f.getTrust())) + .collect(Collectors.joining("\n")); + } + + @Tool(description = "List unresolved fact contradictions detected during Dream consolidation.") + public String fact_list_contradictions( + @ToolParam(description = "Agent ID") Long agentId) { + if (!properties.getFact().isProjectionEnabled()) { + return "Fact projection is disabled."; + } + List contradictions = queryService.listContradictions(agentId); + if (contradictions.isEmpty()) return "No unresolved contradictions."; + + return contradictions.stream() + .map(c -> String.format("- Contradiction #%d: factA=%d vs factB=%d — %s", + c.getId(), c.getFactAId(), c.getFactBId(), + c.getDescription() != null ? c.getDescription() : "")) + .collect(Collectors.joining("\n")); + } +}