mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
feat(wiki): wire layer derivation and fact-dependency persistence into ingest
This commit is contained in:
parent
8e8c1e34c7
commit
55cf2a8458
@ -90,6 +90,26 @@ public class WikiPageTypeProfileService {
|
|||||||
return resolveProfile(kbId).getPageTypes().keySet();
|
return resolveProfile(kbId).getPageTypes().keySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The knowledge layer for a pageType: {@code fact} / {@code experience}.
|
||||||
|
* A known type with no declared layer defaults to {@code fact} (RFC default,
|
||||||
|
* keeps legacy types factual); an unknown/blank type returns {@code null}
|
||||||
|
* so the caller leaves the page's layer untouched.
|
||||||
|
*/
|
||||||
|
public String resolveLayer(Long kbId, String pageType) {
|
||||||
|
WikiPageTypeDef def = resolveProfile(kbId).get(pageType);
|
||||||
|
if (def == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String layer = def.getLayer();
|
||||||
|
return (layer == null || layer.isBlank()) ? "fact" : layer.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether a pageType is in the experience layer (vs fact). */
|
||||||
|
public boolean isExperience(Long kbId, String pageType) {
|
||||||
|
return "experience".equals(resolveLayer(kbId, pageType));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The per-stage LLM instruction declared for a pageType, or empty string.
|
* The per-stage LLM instruction declared for a pageType, or empty string.
|
||||||
* {@code stage} is {@code route} / {@code create} / {@code merge}. Used to
|
* {@code stage} is {@code route} / {@code create} / {@code merge}. Used to
|
||||||
|
|||||||
@ -368,6 +368,16 @@ public class WikiPageService {
|
|||||||
.set(WikiPageEntity::getProfileVersion, profileVersion));
|
.set(WikiPageEntity::getProfileVersion, profileVersion));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set only a page's knowledge layer via a partial update (leaves depends_on untouched). */
|
||||||
|
public void setKnowledgeLayer(Long pageId, String knowledgeLayer) {
|
||||||
|
if (pageId == null || knowledgeLayer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pageMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<WikiPageEntity>()
|
||||||
|
.eq(WikiPageEntity::getId, pageId)
|
||||||
|
.set(WikiPageEntity::getKnowledgeLayer, knowledgeLayer));
|
||||||
|
}
|
||||||
|
|
||||||
/** Set a page's knowledge layer and depends-on snapshot via a partial update. */
|
/** Set a page's knowledge layer and depends-on snapshot via a partial update. */
|
||||||
public void setLayerAndDependencies(Long pageId, String knowledgeLayer, String dependsOnJson) {
|
public void setLayerAndDependencies(Long pageId, String knowledgeLayer, String dependsOnJson) {
|
||||||
if (pageId == null) {
|
if (pageId == null) {
|
||||||
|
|||||||
@ -74,6 +74,10 @@ public class WikiProcessingService {
|
|||||||
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
||||||
private vip.mate.wiki.profile.WikiMetadataValidator metadataValidator;
|
private vip.mate.wiki.profile.WikiMetadataValidator metadataValidator;
|
||||||
|
|
||||||
|
/** Optional dependency/stale engine for layered-knowledge wiring. */
|
||||||
|
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
||||||
|
private vip.mate.wiki.service.WikiDependencyService dependencyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read-the-failover-chain handle. Optional so the existing constructors and
|
* Read-the-failover-chain handle. Optional so the existing constructors and
|
||||||
* lazy-mode tests don't have to thread a new dependency. When null, the
|
* lazy-mode tests don't have to thread a new dependency. When null, the
|
||||||
@ -1170,6 +1174,7 @@ public class WikiProcessingService {
|
|||||||
pageType = pageTypeProfileService.normalizePageType(kbId, pageType);
|
pageType = pageTypeProfileService.normalizePageType(kbId, pageType);
|
||||||
}
|
}
|
||||||
JsonNode metadataNode = pageJson.path("metadata");
|
JsonNode metadataNode = pageJson.path("metadata");
|
||||||
|
JsonNode dependsOnNode = pageJson.path("depends_on");
|
||||||
if (content.isBlank()) {
|
if (content.isBlank()) {
|
||||||
log.info("[Wiki] BatchCreate: blank content for slug='{}', retrying individually", slug);
|
log.info("[Wiki] BatchCreate: blank content for slug='{}', retrying individually", slug);
|
||||||
final String blankSlug = slug;
|
final String blankSlug = slug;
|
||||||
@ -1198,7 +1203,7 @@ public class WikiProcessingService {
|
|||||||
boolean wasCreated = false;
|
boolean wasCreated = false;
|
||||||
boolean ok = false;
|
boolean ok = false;
|
||||||
try {
|
try {
|
||||||
wasCreated = savePageContent(kb, raw, slug, title, content, pageSummary, pageType, metadataNode);
|
wasCreated = savePageContent(kb, raw, slug, title, content, pageSummary, pageType, metadataNode, dependsOnNode);
|
||||||
if (wasCreated) {
|
if (wasCreated) {
|
||||||
created.incrementAndGet();
|
created.incrementAndGet();
|
||||||
totalCreated++;
|
totalCreated++;
|
||||||
@ -1392,18 +1397,24 @@ public class WikiProcessingService {
|
|||||||
*/
|
*/
|
||||||
private boolean savePageContent(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
private boolean savePageContent(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
||||||
String slug, String title, String content, String pageSummary) {
|
String slug, String title, String content, String pageSummary) {
|
||||||
return savePageContent(kb, raw, slug, title, content, pageSummary, null, null);
|
return savePageContent(kb, raw, slug, title, content, pageSummary, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean savePageContent(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
private boolean savePageContent(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
||||||
String slug, String title, String content, String pageSummary,
|
String slug, String title, String content, String pageSummary,
|
||||||
String pageType) {
|
String pageType) {
|
||||||
return savePageContent(kb, raw, slug, title, content, pageSummary, pageType, null);
|
return savePageContent(kb, raw, slug, title, content, pageSummary, pageType, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean savePageContent(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
private boolean savePageContent(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
||||||
String slug, String title, String content, String pageSummary,
|
String slug, String title, String content, String pageSummary,
|
||||||
String pageType, JsonNode metadataNode) {
|
String pageType, JsonNode metadataNode) {
|
||||||
|
return savePageContent(kb, raw, slug, title, content, pageSummary, pageType, metadataNode, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean savePageContent(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
||||||
|
String slug, String title, String content, String pageSummary,
|
||||||
|
String pageType, JsonNode metadataNode, JsonNode dependsOnNode) {
|
||||||
Long kbId = kb.getId();
|
Long kbId = kb.getId();
|
||||||
Long rawId = raw.getId();
|
Long rawId = raw.getId();
|
||||||
|
|
||||||
@ -1418,7 +1429,7 @@ public class WikiProcessingService {
|
|||||||
String actualSlug = existingByCanonical.getSlug();
|
String actualSlug = existingByCanonical.getSlug();
|
||||||
pageService.updatePageByAi(kbId, actualSlug, content, pageSummary, rawId);
|
pageService.updatePageByAi(kbId, actualSlug, content, pageSummary, rawId);
|
||||||
pageService.mergeSourceLineage(existingByCanonical.getId(), rawId, raw.getTitle());
|
pageService.mergeSourceLineage(existingByCanonical.getId(), rawId, raw.getTitle());
|
||||||
afterPagePersisted(existingByCanonical.getId(), kbId, pageType, metadataNode);
|
afterPagePersisted(existingByCanonical.getId(), kbId, pageType, metadataNode, dependsOnNode);
|
||||||
log.info("[Wiki] Phase B create slug='{}' canonical-matches existing '{}', updated",
|
log.info("[Wiki] Phase B create slug='{}' canonical-matches existing '{}', updated",
|
||||||
slug, actualSlug);
|
slug, actualSlug);
|
||||||
return false;
|
return false;
|
||||||
@ -1435,7 +1446,7 @@ public class WikiProcessingService {
|
|||||||
if (winner != null) {
|
if (winner != null) {
|
||||||
pageService.updatePageByAi(kbId, winnerSlug, content, pageSummary, rawId);
|
pageService.updatePageByAi(kbId, winnerSlug, content, pageSummary, rawId);
|
||||||
pageService.mergeSourceLineage(winner.getId(), rawId, raw.getTitle());
|
pageService.mergeSourceLineage(winner.getId(), rawId, raw.getTitle());
|
||||||
afterPagePersisted(winner.getId(), kbId, pageType, metadataNode);
|
afterPagePersisted(winner.getId(), kbId, pageType, metadataNode, dependsOnNode);
|
||||||
log.info("[Wiki] Phase B create slug='{}' lost slug-claim race to '{}', updated",
|
log.info("[Wiki] Phase B create slug='{}' lost slug-claim race to '{}', updated",
|
||||||
slug, winnerSlug);
|
slug, winnerSlug);
|
||||||
return false;
|
return false;
|
||||||
@ -1451,7 +1462,7 @@ public class WikiProcessingService {
|
|||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
pageService.updatePageByAi(kbId, slug, content, pageSummary, rawId);
|
pageService.updatePageByAi(kbId, slug, content, pageSummary, rawId);
|
||||||
pageService.mergeSourceLineage(existing.getId(), rawId, raw.getTitle());
|
pageService.mergeSourceLineage(existing.getId(), rawId, raw.getTitle());
|
||||||
afterPagePersisted(existing.getId(), kbId, pageType, metadataNode);
|
afterPagePersisted(existing.getId(), kbId, pageType, metadataNode, dependsOnNode);
|
||||||
log.info("[Wiki] Phase B create page slug='{}' done (updated existing)", slug);
|
log.info("[Wiki] Phase B create page slug='{}' done (updated existing)", slug);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1460,7 +1471,7 @@ public class WikiProcessingService {
|
|||||||
try {
|
try {
|
||||||
WikiPageEntity created = pageService.createPage(kbId, slug, title, content, pageSummary, sourceRawIds, pageType);
|
WikiPageEntity created = pageService.createPage(kbId, slug, title, content, pageSummary, sourceRawIds, pageType);
|
||||||
pageService.mergeSourceLineage(created.getId(), rawId, raw.getTitle());
|
pageService.mergeSourceLineage(created.getId(), rawId, raw.getTitle());
|
||||||
afterPagePersisted(created.getId(), kbId, pageType, metadataNode);
|
afterPagePersisted(created.getId(), kbId, pageType, metadataNode, dependsOnNode);
|
||||||
log.info("[Wiki] Phase B create page slug='{}' done (created)", slug);
|
log.info("[Wiki] Phase B create page slug='{}' done (created)", slug);
|
||||||
citationService.buildCitationsAsync(created.getId(), kbId);
|
citationService.buildCitationsAsync(created.getId(), kbId);
|
||||||
return true;
|
return true;
|
||||||
@ -1469,7 +1480,7 @@ public class WikiProcessingService {
|
|||||||
pageService.updatePageByAi(kbId, slug, content, pageSummary, rawId);
|
pageService.updatePageByAi(kbId, slug, content, pageSummary, rawId);
|
||||||
WikiPageEntity raced = pageService.getBySlug(kbId, slug);
|
WikiPageEntity raced = pageService.getBySlug(kbId, slug);
|
||||||
if (raced != null) {
|
if (raced != null) {
|
||||||
afterPagePersisted(raced.getId(), kbId, pageType, metadataNode);
|
afterPagePersisted(raced.getId(), kbId, pageType, metadataNode, dependsOnNode);
|
||||||
}
|
}
|
||||||
log.info("[Wiki] Phase B create page slug='{}' lost INSERT race -> updated existing", slug);
|
log.info("[Wiki] Phase B create page slug='{}' lost INSERT race -> updated existing", slug);
|
||||||
return false;
|
return false;
|
||||||
@ -1488,8 +1499,11 @@ public class WikiProcessingService {
|
|||||||
* one outlet means the existing-page-update and race-arbitration paths get
|
* one outlet means the existing-page-update and race-arbitration paths get
|
||||||
* the same metadata and trigger handling as a clean create.
|
* the same metadata and trigger handling as a clean create.
|
||||||
*/
|
*/
|
||||||
private void afterPagePersisted(Long pageId, Long kbId, String pageType, JsonNode metadataNode) {
|
private void afterPagePersisted(Long pageId, Long kbId, String pageType,
|
||||||
|
JsonNode metadataNode, JsonNode dependsOnNode) {
|
||||||
applyValidatedMetadata(pageId, kbId, pageType, metadataNode);
|
applyValidatedMetadata(pageId, kbId, pageType, metadataNode);
|
||||||
|
deriveKnowledgeLayer(pageId, kbId, pageType);
|
||||||
|
applyDependencies(pageId, kbId, pageType, dependsOnNode);
|
||||||
// The page is committed (createPage / updatePageByAi are their own
|
// The page is committed (createPage / updatePageByAi are their own
|
||||||
// transactions), so the count is accurate. Idempotent + dedup-guarded
|
// transactions), so the count is accurate. Idempotent + dedup-guarded
|
||||||
// downstream, so firing on update paths is safe.
|
// downstream, so firing on update paths is safe.
|
||||||
@ -1498,6 +1512,50 @@ public class WikiProcessingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Stamp the page's knowledge layer (fact/experience) derived from its pageType profile. */
|
||||||
|
private void deriveKnowledgeLayer(Long pageId, Long kbId, String pageType) {
|
||||||
|
if (pageTypeProfileService == null || pageType == null || pageType.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String layer = pageTypeProfileService.resolveLayer(kbId, pageType);
|
||||||
|
if (layer != null) {
|
||||||
|
pageService.setKnowledgeLayer(pageId, layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist an experience page's fact dependencies declared by the LLM
|
||||||
|
* ({@code depends_on}: slugs). Resolves slugs to ids and delegates to the
|
||||||
|
* dependency engine, which rejects cross-KB / non-fact / missing targets;
|
||||||
|
* rejections are logged as a warning (non-blocking, MVP).
|
||||||
|
*/
|
||||||
|
private void applyDependencies(Long pageId, Long kbId, String pageType, JsonNode dependsOnNode) {
|
||||||
|
if (dependencyService == null || pageTypeProfileService == null
|
||||||
|
|| dependsOnNode == null || !dependsOnNode.isArray() || dependsOnNode.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!pageTypeProfileService.isExperience(kbId, pageType)) {
|
||||||
|
return; // only experience pages declare fact dependencies
|
||||||
|
}
|
||||||
|
java.util.List<Long> depIds = new java.util.ArrayList<>();
|
||||||
|
for (JsonNode n : dependsOnNode) {
|
||||||
|
String slug = n.asText("");
|
||||||
|
if (slug.isBlank()) continue;
|
||||||
|
WikiPageEntity dep = pageService.getBySlug(kbId, slug);
|
||||||
|
if (dep != null) {
|
||||||
|
depIds.add(dep.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
java.util.List<String> rejected = dependencyService.setDependencies(kbId, pageId, depIds);
|
||||||
|
if (!rejected.isEmpty()) {
|
||||||
|
log.warn("[Wiki] page {} dependency warnings: {}", pageId, rejected);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[Wiki] dependency persistence failed for page {}: {}", pageId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String emptyOr(String s) {
|
private String emptyOr(String s) {
|
||||||
return (s == null || s.isBlank()) ? "(无)" : s;
|
return (s == null || s.isBlank()) ? "(无)" : s;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
- `summary`:一段话简短摘要
|
- `summary`:一段话简短摘要
|
||||||
- `page_type`:页面类型,从下面"允许的页面类型"列表中选一个;都不合适时选 concept。
|
- `page_type`:页面类型,从下面"允许的页面类型"列表中选一个;都不合适时选 concept。
|
||||||
- `metadata`(可选):与所选 page_type 对应的结构化字段对象,只输出该类型声明的字段(带"required metadata"标注的字段应尽量补全)。
|
- `metadata`(可选):与所选 page_type 对应的结构化字段对象,只输出该类型声明的字段(带"required metadata"标注的字段应尽量补全)。
|
||||||
|
- `depends_on`(可选,仅经验层页面需要):本页所依赖的**事实层页面 slug 数组**。经验层(如 analysis/pattern/regime)必须列出其结论所基于的事实页 slug;事实层页面留空或不输出。
|
||||||
|
|
||||||
允许的页面类型:
|
允许的页面类型:
|
||||||
{allowed_page_types}
|
{allowed_page_types}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import vip.mate.wiki.repository.WikiPageTypeProfileMapper;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@ -89,6 +90,26 @@ class WikiPageTypeProfileServiceTest {
|
|||||||
assertTrue(fragment.contains("- other"), fragment);
|
assertTrue(fragment.contains("- other"), fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveLayer_defaultsKnownTypesToFact_experienceWhenDeclared() {
|
||||||
|
WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity();
|
||||||
|
row.setKbId(1L);
|
||||||
|
row.setEnabled(1);
|
||||||
|
row.setConfigJson("{\"pageTypes\":{"
|
||||||
|
+ "\"episode\":{\"layer\":\"fact\"},"
|
||||||
|
+ "\"concept\":{}," // known type, no layer → defaults to fact
|
||||||
|
+ "\"pattern\":{\"layer\":\"experience\"}}}");
|
||||||
|
when(mapper.selectOne(any())).thenReturn(row);
|
||||||
|
|
||||||
|
assertEquals("fact", service.resolveLayer(1L, "episode"));
|
||||||
|
assertEquals("fact", service.resolveLayer(1L, "concept"));
|
||||||
|
assertEquals("experience", service.resolveLayer(1L, "pattern"));
|
||||||
|
assertTrue(service.isExperience(1L, "pattern"));
|
||||||
|
assertFalse(service.isExperience(1L, "episode"));
|
||||||
|
// unknown type → null (caller leaves layer untouched)
|
||||||
|
assertNull(service.resolveLayer(1L, "made-up"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void stageInstructionAndTemplate_areResolvedPerType() {
|
void stageInstructionAndTemplate_areResolvedPerType() {
|
||||||
WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity();
|
WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user