From b50e384e0df4e0d4f51fbafc94e5f1e913233242 Mon Sep 17 00:00:00 2001 From: matevip Date: Sun, 31 May 2026 07:54:34 +0800 Subject: [PATCH] feat(wiki): inject KB pageType profile into the batch-create prompt --- .../profile/WikiPageTypeProfileService.java | 30 +++++++++++++++++++ .../wiki/service/WikiProcessingService.java | 18 +++++++++++ .../prompts/wiki/batch-create-system.txt | 6 +++- .../WikiPageTypeProfileServiceTest.java | 25 ++++++++++++++++ 4 files changed, 78 insertions(+), 1 deletion(-) diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/profile/WikiPageTypeProfileService.java b/mateclaw-server/src/main/java/vip/mate/wiki/profile/WikiPageTypeProfileService.java index eac958e4..a9427cd8 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/profile/WikiPageTypeProfileService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/profile/WikiPageTypeProfileService.java @@ -10,6 +10,7 @@ import vip.mate.wiki.model.WikiPageTypeProfileEntity; import vip.mate.wiki.repository.WikiPageTypeProfileMapper; import java.io.InputStream; +import java.util.Map; import java.util.Set; /** @@ -89,6 +90,35 @@ public class WikiPageTypeProfileService { return resolveProfile(kbId).getPageTypes().keySet(); } + /** + * Render the KB's allowed page types as a prompt fragment, one per line + * with description and required-metadata hints, e.g. + * {@code - episode: a dated event (required metadata: event_type, event_date)}. + * Injected into the route / batch-create prompts so the LLM only emits + * types the KB recognises. + */ + public String describeForPrompt(Long kbId) { + WikiPageTypeProfile profile = resolveProfile(kbId); + StringBuilder sb = new StringBuilder(); + profile.getPageTypes().forEach((name, def) -> { + sb.append("- ").append(name); + if (def != null && def.getDescription() != null && !def.getDescription().isBlank()) { + sb.append(": ").append(def.getDescription().trim()); + } + if (def != null && def.getSchema() != null) { + java.util.List required = def.getSchema().entrySet().stream() + .filter(e -> e.getValue() != null && e.getValue().isRequired()) + .map(Map.Entry::getKey) + .toList(); + if (!required.isEmpty()) { + sb.append(" (required metadata: ").append(String.join(", ", required)).append(")"); + } + } + sb.append('\n'); + }); + return sb.toString().trim(); + } + /** The enabled profile row for a KB, or {@code null} when none configured. */ public WikiPageTypeProfileEntity findEnabledRow(Long kbId) { if (kbId == null) { diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java index a97dc442..8650d3a7 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java @@ -62,6 +62,14 @@ public class WikiProcessingService { private final WikiCitationService citationService; private final org.springframework.context.ApplicationEventPublisher eventPublisher; + /** + * Optional KB pageType profile. Field-injected (not a constructor arg) so + * existing instantiations are unaffected; when absent the batch-create + * prompt falls back to the legacy hardcoded pageType enum. + */ + @org.springframework.beans.factory.annotation.Autowired(required = false) + private vip.mate.wiki.profile.WikiPageTypeProfileService pageTypeProfileService; + /** * 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 @@ -1065,6 +1073,16 @@ public class WikiProcessingService { metasJson.append("]"); String batchSystem = PromptLoader.loadPrompt("wiki/batch-create-system"); + if (pageTypeProfileService != null) { + // Inject the KB's allowed page types so the LLM only emits types + // the profile recognises. Default-profile KBs get the same list + // as the previous hardcoded enum, so behaviour is unchanged. + batchSystem = batchSystem.replace("{allowed_page_types}", + pageTypeProfileService.describeForPrompt(kbId)); + } else { + batchSystem = batchSystem.replace("{allowed_page_types}", + "concept / person / place / event / technology / organization / product / term / process / other"); + } String batchUserTemplate = PromptLoader.loadPrompt("wiki/batch-create-user"); String docMapSection = buildDocumentMapSection(documentMap); String batchUser = batchUserTemplate diff --git a/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt b/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt index ab217c4d..67be8f0b 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt @@ -50,4 +50,8 @@ - `title`:与 metadata 保持一致;如有更精确的描述可微调 - `content`:完整 markdown 正文 - `summary`:一段话简短摘要 -- `page_type`:页面类型,从以下值中选一个:concept / person / place / event / technology / organization / product / term / process / other +- `page_type`:页面类型,从下面"允许的页面类型"列表中选一个;都不合适时选 concept。 +- `metadata`(可选):与所选 page_type 对应的结构化字段对象,只输出该类型声明的字段(带"required metadata"标注的字段应尽量补全)。 + +允许的页面类型: +{allowed_page_types} diff --git a/mateclaw-server/src/test/java/vip/mate/wiki/profile/WikiPageTypeProfileServiceTest.java b/mateclaw-server/src/test/java/vip/mate/wiki/profile/WikiPageTypeProfileServiceTest.java index 6f9a15b2..01c30f2b 100644 --- a/mateclaw-server/src/test/java/vip/mate/wiki/profile/WikiPageTypeProfileServiceTest.java +++ b/mateclaw-server/src/test/java/vip/mate/wiki/profile/WikiPageTypeProfileServiceTest.java @@ -79,4 +79,29 @@ class WikiPageTypeProfileServiceTest { assertEquals("concept", service.normalizePageType(1L, "made-up-type")); assertEquals("concept", service.normalizePageType(1L, null)); } + + @Test + void describeForPrompt_defaultProfile_listsBuiltInTypes() { + when(mapper.selectOne(any())).thenReturn(null); + String fragment = service.describeForPrompt(1L); + assertTrue(fragment.contains("- concept"), fragment); + assertTrue(fragment.contains("- person"), fragment); + assertTrue(fragment.contains("- other"), fragment); + } + + @Test + void describeForPrompt_customProfile_showsRequiredMetadata() { + WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity(); + row.setKbId(1L); + row.setEnabled(1); + row.setConfigJson("{\"pageTypes\":{\"episode\":{\"description\":\"a dated event\"," + + "\"schema\":{\"event_date\":{\"type\":\"date\",\"required\":true}," + + "\"note\":{\"type\":\"string\",\"required\":false}}}}}"); + when(mapper.selectOne(any())).thenReturn(row); + + String fragment = service.describeForPrompt(1L); + assertTrue(fragment.contains("- episode: a dated event"), fragment); + assertTrue(fragment.contains("required metadata: event_date"), fragment); + assertFalse(fragment.contains("note"), fragment); // optional field not listed as required + } }