mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* {@code stage} is {@code route} / {@code create} / {@code merge}. Used to
|
||||
|
||||
@ -368,6 +368,16 @@ public class WikiPageService {
|
||||
.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. */
|
||||
public void setLayerAndDependencies(Long pageId, String knowledgeLayer, String dependsOnJson) {
|
||||
if (pageId == null) {
|
||||
|
||||
@ -74,6 +74,10 @@ public class WikiProcessingService {
|
||||
@org.springframework.beans.factory.annotation.Autowired(required = false)
|
||||
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
|
||||
* 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);
|
||||
}
|
||||
JsonNode metadataNode = pageJson.path("metadata");
|
||||
JsonNode dependsOnNode = pageJson.path("depends_on");
|
||||
if (content.isBlank()) {
|
||||
log.info("[Wiki] BatchCreate: blank content for slug='{}', retrying individually", slug);
|
||||
final String blankSlug = slug;
|
||||
@ -1198,7 +1203,7 @@ public class WikiProcessingService {
|
||||
boolean wasCreated = false;
|
||||
boolean ok = false;
|
||||
try {
|
||||
wasCreated = savePageContent(kb, raw, slug, title, content, pageSummary, pageType, metadataNode);
|
||||
wasCreated = savePageContent(kb, raw, slug, title, content, pageSummary, pageType, metadataNode, dependsOnNode);
|
||||
if (wasCreated) {
|
||||
created.incrementAndGet();
|
||||
totalCreated++;
|
||||
@ -1392,18 +1397,24 @@ public class WikiProcessingService {
|
||||
*/
|
||||
private boolean savePageContent(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
||||
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,
|
||||
String slug, String title, String content, String pageSummary,
|
||||
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,
|
||||
String slug, String title, String content, String pageSummary,
|
||||
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 rawId = raw.getId();
|
||||
|
||||
@ -1418,7 +1429,7 @@ public class WikiProcessingService {
|
||||
String actualSlug = existingByCanonical.getSlug();
|
||||
pageService.updatePageByAi(kbId, actualSlug, content, pageSummary, rawId);
|
||||
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",
|
||||
slug, actualSlug);
|
||||
return false;
|
||||
@ -1435,7 +1446,7 @@ public class WikiProcessingService {
|
||||
if (winner != null) {
|
||||
pageService.updatePageByAi(kbId, winnerSlug, content, pageSummary, rawId);
|
||||
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",
|
||||
slug, winnerSlug);
|
||||
return false;
|
||||
@ -1451,7 +1462,7 @@ public class WikiProcessingService {
|
||||
if (existing != null) {
|
||||
pageService.updatePageByAi(kbId, slug, content, pageSummary, rawId);
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
@ -1460,7 +1471,7 @@ public class WikiProcessingService {
|
||||
try {
|
||||
WikiPageEntity created = pageService.createPage(kbId, slug, title, content, pageSummary, sourceRawIds, pageType);
|
||||
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);
|
||||
citationService.buildCitationsAsync(created.getId(), kbId);
|
||||
return true;
|
||||
@ -1469,7 +1480,7 @@ public class WikiProcessingService {
|
||||
pageService.updatePageByAi(kbId, slug, content, pageSummary, rawId);
|
||||
WikiPageEntity raced = pageService.getBySlug(kbId, slug);
|
||||
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);
|
||||
return false;
|
||||
@ -1488,8 +1499,11 @@ public class WikiProcessingService {
|
||||
* one outlet means the existing-page-update and race-arbitration paths get
|
||||
* 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);
|
||||
deriveKnowledgeLayer(pageId, kbId, pageType);
|
||||
applyDependencies(pageId, kbId, pageType, dependsOnNode);
|
||||
// The page is committed (createPage / updatePageByAi are their own
|
||||
// transactions), so the count is accurate. Idempotent + dedup-guarded
|
||||
// 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) {
|
||||
return (s == null || s.isBlank()) ? "(无)" : s;
|
||||
}
|
||||
|
||||
@ -52,6 +52,7 @@
|
||||
- `summary`:一段话简短摘要
|
||||
- `page_type`:页面类型,从下面"允许的页面类型"列表中选一个;都不合适时选 concept。
|
||||
- `metadata`(可选):与所选 page_type 对应的结构化字段对象,只输出该类型声明的字段(带"required metadata"标注的字段应尽量补全)。
|
||||
- `depends_on`(可选,仅经验层页面需要):本页所依赖的**事实层页面 slug 数组**。经验层(如 analysis/pattern/regime)必须列出其结论所基于的事实页 slug;事实层页面留空或不输出。
|
||||
|
||||
允许的页面类型:
|
||||
{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.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@ -89,6 +90,26 @@ class WikiPageTypeProfileServiceTest {
|
||||
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
|
||||
void stageInstructionAndTemplate_areResolvedPerType() {
|
||||
WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user