mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(memory): dream-v2 E2 — Fact query tools + FactMemoryProvider
This commit is contained in:
parent
acc6f448db
commit
d983a1e02e
@ -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.
|
||||
* <p>
|
||||
* - 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<FactEntity> 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 "<facts>\n" + block + "\n</facts>";
|
||||
}
|
||||
|
||||
@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<Object> getToolBeans() {
|
||||
if (!properties.getFact().isProjectionEnabled()) return Collections.emptyList();
|
||||
return List.of(factQueryTool);
|
||||
}
|
||||
}
|
||||
@ -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<FactEntity> probe(Long agentId, String entity) {
|
||||
return factMapper.selectList(
|
||||
new LambdaQueryWrapper<FactEntity>()
|
||||
.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<FactEntity> related(Long agentId, String entity, int hops) {
|
||||
// Find fact IDs that reference this entity
|
||||
List<FactEntityRefEntity> refs = refMapper.selectList(
|
||||
new LambdaQueryWrapper<FactEntityRefEntity>()
|
||||
.like(FactEntityRefEntity::getEntityName, entity)
|
||||
.last("LIMIT 50"));
|
||||
List<Long> factIds = refs.stream().map(FactEntityRefEntity::getFactId).distinct().toList();
|
||||
if (factIds.isEmpty()) return List.of();
|
||||
|
||||
return factMapper.selectList(
|
||||
new LambdaQueryWrapper<FactEntity>()
|
||||
.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<FactContradictionEntity> listContradictions(Long agentId) {
|
||||
return contradictionMapper.selectList(
|
||||
new LambdaQueryWrapper<FactContradictionEntity>()
|
||||
.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<FactEntity> recallRelevant(Long agentId, String query) {
|
||||
return factMapper.selectList(
|
||||
new LambdaQueryWrapper<FactEntity>()
|
||||
.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<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) return;
|
||||
factMapper.bumpUseCount(ids, LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
@ -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<FactContradictionEntity> {
|
||||
}
|
||||
@ -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<FactEntityRefEntity> {
|
||||
}
|
||||
@ -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<FactEntity> 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<FactEntity> 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<FactContradictionEntity> 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"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user