mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 20:34:39 +08:00
feat(wiki): inject pageType profile into route/create/merge prompts + content templates
This commit is contained in:
parent
551321b542
commit
8e8c1e34c7
@ -90,6 +90,56 @@ public class WikiPageTypeProfileService {
|
|||||||
return resolveProfile(kbId).getPageTypes().keySet();
|
return resolveProfile(kbId).getPageTypes().keySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The per-stage LLM instruction declared for a pageType, or empty string.
|
||||||
|
* {@code stage} is {@code route} / {@code create} / {@code merge}. Used to
|
||||||
|
* inject type-specific guidance into the corresponding stage prompt.
|
||||||
|
*/
|
||||||
|
public String stageInstruction(Long kbId, String pageType, String stage) {
|
||||||
|
WikiPageTypeDef def = resolveProfile(kbId).get(pageType);
|
||||||
|
if (def == null || stage == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
WikiPageTypeDef.StageInstructions si = switch (stage) {
|
||||||
|
case "route" -> def.getRoute();
|
||||||
|
case "create" -> def.getCreate();
|
||||||
|
case "merge" -> def.getMerge();
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
return (si != null && si.getInstructions() != null) ? si.getInstructions().trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the per-type Markdown templates as a prompt block for the
|
||||||
|
* multi-page batch-create stage (where one call generates pages of several
|
||||||
|
* types). Only types that declare a template are listed; empty when none.
|
||||||
|
*/
|
||||||
|
public String describeTemplatesForPrompt(Long kbId) {
|
||||||
|
WikiPageTypeProfile profile = resolveProfile(kbId);
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
profile.getPageTypes().forEach((name, def) -> {
|
||||||
|
if (def != null && def.getTemplate() != null
|
||||||
|
&& def.getTemplate().getMarkdown() != null
|
||||||
|
&& !def.getTemplate().getMarkdown().isBlank()) {
|
||||||
|
sb.append("### ").append(name).append(" 骨架\n")
|
||||||
|
.append(def.getTemplate().getMarkdown().trim()).append("\n\n");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Markdown content template for a pageType, or empty string. Injected
|
||||||
|
* into the generation prompt so the page follows the declared skeleton.
|
||||||
|
*/
|
||||||
|
public String templateMarkdown(Long kbId, String pageType) {
|
||||||
|
WikiPageTypeDef def = resolveProfile(kbId).get(pageType);
|
||||||
|
if (def == null || def.getTemplate() == null || def.getTemplate().getMarkdown() == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return def.getTemplate().getMarkdown().trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the KB's allowed page types as a prompt fragment, one per line
|
* Render the KB's allowed page types as a prompt fragment, one per line
|
||||||
* with description and required-metadata hints, e.g.
|
* with description and required-metadata hints, e.g.
|
||||||
|
|||||||
@ -813,7 +813,8 @@ public class WikiProcessingService {
|
|||||||
// this picks up changes from sequential chunks without an extra DB hit when nothing changed.
|
// this picks up changes from sequential chunks without an extra DB hit when nothing changed.
|
||||||
String freshIndex = buildExistingPagesIndex(kbId);
|
String freshIndex = buildExistingPagesIndex(kbId);
|
||||||
|
|
||||||
String routeSystem = PromptLoader.loadPrompt("wiki/route-system");
|
String routeSystem = PromptLoader.loadPrompt("wiki/route-system")
|
||||||
|
.replace("{allowed_page_types}", allowedTypesFragment(kbId));
|
||||||
String routeUserTemplate = PromptLoader.loadPrompt("wiki/route-user");
|
String routeUserTemplate = PromptLoader.loadPrompt("wiki/route-user");
|
||||||
String documentMapSection = buildDocumentMapSection(documentMap);
|
String documentMapSection = buildDocumentMapSection(documentMap);
|
||||||
String routeUser = routeUserTemplate
|
String routeUser = routeUserTemplate
|
||||||
@ -1082,10 +1083,13 @@ public class WikiProcessingService {
|
|||||||
// the profile recognises. Default-profile KBs get the same list
|
// the profile recognises. Default-profile KBs get the same list
|
||||||
// as the previous hardcoded enum, so behaviour is unchanged.
|
// as the previous hardcoded enum, so behaviour is unchanged.
|
||||||
batchSystem = batchSystem.replace("{allowed_page_types}",
|
batchSystem = batchSystem.replace("{allowed_page_types}",
|
||||||
pageTypeProfileService.describeForPrompt(kbId));
|
pageTypeProfileService.describeForPrompt(kbId))
|
||||||
|
.replace("{page_type_templates}",
|
||||||
|
emptyOr(pageTypeProfileService.describeTemplatesForPrompt(kbId)));
|
||||||
} else {
|
} else {
|
||||||
batchSystem = batchSystem.replace("{allowed_page_types}",
|
batchSystem = batchSystem.replace("{allowed_page_types}",
|
||||||
"concept / person / place / event / technology / organization / product / term / process / other");
|
"concept / person / place / event / technology / organization / product / term / process / other")
|
||||||
|
.replace("{page_type_templates}", "(无)");
|
||||||
}
|
}
|
||||||
String batchUserTemplate = PromptLoader.loadPrompt("wiki/batch-create-user");
|
String batchUserTemplate = PromptLoader.loadPrompt("wiki/batch-create-user");
|
||||||
String docMapSection = buildDocumentMapSection(documentMap);
|
String docMapSection = buildDocumentMapSection(documentMap);
|
||||||
@ -1264,7 +1268,9 @@ public class WikiProcessingService {
|
|||||||
String title = pageMeta.path("title").asText("");
|
String title = pageMeta.path("title").asText("");
|
||||||
String summary = pageMeta.path("summary").asText("");
|
String summary = pageMeta.path("summary").asText("");
|
||||||
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
|
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
|
||||||
String createSystem = PromptLoader.loadPrompt("wiki/create-page-system");
|
String createSystem = PromptLoader.loadPrompt("wiki/create-page-system")
|
||||||
|
.replace("{page_type_instructions}",
|
||||||
|
typeGuidance(kb.getId(), pageMeta.path("page_type").asText(""), "create"));
|
||||||
String createUserTemplate = PromptLoader.loadPrompt("wiki/create-page-user");
|
String createUserTemplate = PromptLoader.loadPrompt("wiki/create-page-user");
|
||||||
String createUser = createUserTemplate
|
String createUser = createUserTemplate
|
||||||
.replace("{config}", configContent)
|
.replace("{config}", configContent)
|
||||||
@ -1492,6 +1498,38 @@ public class WikiProcessingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String emptyOr(String s) {
|
||||||
|
return (s == null || s.isBlank()) ? "(无)" : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Allowed page types fragment for prompt injection (profile-driven; legacy fallback). */
|
||||||
|
private String allowedTypesFragment(Long kbId) {
|
||||||
|
return pageTypeProfileService != null
|
||||||
|
? pageTypeProfileService.describeForPrompt(kbId)
|
||||||
|
: "concept / person / place / event / technology / organization / product / term / process / other";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-type guidance for the create / merge prompts: the stage instruction
|
||||||
|
* plus, for the create stage, the Markdown template skeleton. Empty-safe.
|
||||||
|
*/
|
||||||
|
private String typeGuidance(Long kbId, String pageType, String stage) {
|
||||||
|
if (pageTypeProfileService == null || pageType == null || pageType.isBlank()) {
|
||||||
|
return "(无特定指引)";
|
||||||
|
}
|
||||||
|
String instr = pageTypeProfileService.stageInstruction(kbId, pageType, stage);
|
||||||
|
String tpl = "create".equals(stage) ? pageTypeProfileService.templateMarkdown(kbId, pageType) : "";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (instr != null && !instr.isBlank()) {
|
||||||
|
sb.append(instr);
|
||||||
|
}
|
||||||
|
if (tpl != null && !tpl.isBlank()) {
|
||||||
|
if (sb.length() > 0) sb.append("\n\n");
|
||||||
|
sb.append("请按以下 Markdown 骨架组织正文:\n").append(tpl);
|
||||||
|
}
|
||||||
|
return sb.length() == 0 ? "(无特定指引)" : sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private void applyValidatedMetadata(Long pageId, Long kbId, String pageType,
|
private void applyValidatedMetadata(Long pageId, Long kbId, String pageType,
|
||||||
JsonNode metadataNode) {
|
JsonNode metadataNode) {
|
||||||
if (pageId == null || pageTypeProfileService == null || metadataValidator == null) {
|
if (pageId == null || pageTypeProfileService == null || metadataValidator == null) {
|
||||||
@ -1546,7 +1584,9 @@ public class WikiProcessingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
|
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
|
||||||
String mergeSystem = PromptLoader.loadPrompt("wiki/merge-page-system");
|
String mergeSystem = PromptLoader.loadPrompt("wiki/merge-page-system")
|
||||||
|
.replace("{page_type_merge_instruction}",
|
||||||
|
typeGuidance(kbId, existing.getPageType(), "merge"));
|
||||||
// Trim existing content to prevent context overflow on small models (qwen-turbo: 4096 tokens).
|
// Trim existing content to prevent context overflow on small models (qwen-turbo: 4096 tokens).
|
||||||
// Merging a 3000-char page + 30K chunk blows past the limit → truncated JSON → parse failure.
|
// Merging a 3000-char page + 30K chunk blows past the limit → truncated JSON → parse failure.
|
||||||
// 1800 chars ≈ ~600 tokens, leaving ample room for the chunk and response.
|
// 1800 chars ≈ ~600 tokens, leaving ample room for the chunk and response.
|
||||||
@ -2348,7 +2388,9 @@ public class WikiProcessingService {
|
|||||||
// Use existing two-phase single-page create logic
|
// Use existing two-phase single-page create logic
|
||||||
String existingPagesIndex = buildExistingPagesIndex(kb.getId());
|
String existingPagesIndex = buildExistingPagesIndex(kb.getId());
|
||||||
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
|
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
|
||||||
String createSystem = PromptLoader.loadPrompt("wiki/create-page-system");
|
String createSystem = PromptLoader.loadPrompt("wiki/create-page-system")
|
||||||
|
.replace("{page_type_instructions}",
|
||||||
|
typeGuidance(kb.getId(), page.getPageType(), "create"));
|
||||||
String createUserTemplate = PromptLoader.loadPrompt("wiki/create-page-user");
|
String createUserTemplate = PromptLoader.loadPrompt("wiki/create-page-user");
|
||||||
String createUser = createUserTemplate
|
String createUser = createUserTemplate
|
||||||
.replace("{config}", configContent)
|
.replace("{config}", configContent)
|
||||||
|
|||||||
@ -55,3 +55,6 @@
|
|||||||
|
|
||||||
允许的页面类型:
|
允许的页面类型:
|
||||||
{allowed_page_types}
|
{allowed_page_types}
|
||||||
|
|
||||||
|
各类型内容模板(若某页的 page_type 有对应骨架,请按骨架组织正文;无则自由组织):
|
||||||
|
{page_type_templates}
|
||||||
|
|||||||
@ -55,3 +55,6 @@
|
|||||||
- `title`:通常与 metadata 一致;如有更精确的描述可微调
|
- `title`:通常与 metadata 一致;如有更精确的描述可微调
|
||||||
- `content`:完整 markdown,开头一段摘要,后面分章节
|
- `content`:完整 markdown,开头一段摘要,后面分章节
|
||||||
- `summary`:一段话简短摘要(可与 metadata.summary 一致或精炼)
|
- `summary`:一段话简短摘要(可与 metadata.summary 一致或精炼)
|
||||||
|
|
||||||
|
## 内容指引(本知识库 / 该页类型)
|
||||||
|
{page_type_instructions}
|
||||||
|
|||||||
@ -47,3 +47,6 @@
|
|||||||
- `title`:通常保持不变;只有当新材料明确改变了页面主题时才改
|
- `title`:通常保持不变;只有当新材料明确改变了页面主题时才改
|
||||||
- `content`:完整 markdown,包含原内容中仍然有效的部分 + 来自新材料的新增信息
|
- `content`:完整 markdown,包含原内容中仍然有效的部分 + 来自新材料的新增信息
|
||||||
- `summary`:更新后的一段话摘要
|
- `summary`:更新后的一段话摘要
|
||||||
|
|
||||||
|
## 该类型的合并策略(本知识库)
|
||||||
|
{page_type_merge_instruction}
|
||||||
|
|||||||
@ -68,6 +68,12 @@
|
|||||||
- `update`:已存在但需要根据本材料合并更新的页面,**只列 slug 字符串数组,最多 5 条**
|
- `update`:已存在但需要根据本材料合并更新的页面,**只列 slug 字符串数组,最多 5 条**
|
||||||
- 两个数组合计通常至少 1 条(材料完全无价值——如空白页、纯目录——才允许全空)
|
- 两个数组合计通常至少 1 条(材料完全无价值——如空白页、纯目录——才允许全空)
|
||||||
|
|
||||||
|
## 允许的页面类型(本知识库)
|
||||||
|
|
||||||
|
为每个 create 项判断最贴切的类型;不确定时按默认处理。可在 create 项加 `page_type` 字段。
|
||||||
|
|
||||||
|
{allowed_page_types}
|
||||||
|
|
||||||
## 关键纪律
|
## 关键纪律
|
||||||
|
|
||||||
- 输出体积应该是几百到几千字,**不要超过几 KB**。如果你发现自己在写正文,立刻停下 —— 那是下一阶段的工作。
|
- 输出体积应该是几百到几千字,**不要超过几 KB**。如果你发现自己在写正文,立刻停下 —— 那是下一阶段的工作。
|
||||||
|
|||||||
@ -89,6 +89,27 @@ class WikiPageTypeProfileServiceTest {
|
|||||||
assertTrue(fragment.contains("- other"), fragment);
|
assertTrue(fragment.contains("- other"), fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stageInstructionAndTemplate_areResolvedPerType() {
|
||||||
|
WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity();
|
||||||
|
row.setKbId(1L);
|
||||||
|
row.setEnabled(1);
|
||||||
|
row.setConfigJson("{\"pageTypes\":{\"episode\":{"
|
||||||
|
+ "\"route\":{\"instructions\":\"仅当有明确日期时路由为 episode\"},"
|
||||||
|
+ "\"merge\":{\"instructions\":\"保留已审阅分析,追加新证据\"},"
|
||||||
|
+ "\"create\":{\"instructions\":\"抽取 event_date\"},"
|
||||||
|
+ "\"template\":{\"markdown\":\"## Summary\\n{{summary}}\\n## 事件\"}}}}");
|
||||||
|
when(mapper.selectOne(any())).thenReturn(row);
|
||||||
|
|
||||||
|
assertTrue(service.stageInstruction(1L, "episode", "route").contains("明确日期"));
|
||||||
|
assertTrue(service.stageInstruction(1L, "episode", "merge").contains("已审阅"));
|
||||||
|
assertTrue(service.stageInstruction(1L, "episode", "create").contains("event_date"));
|
||||||
|
assertTrue(service.templateMarkdown(1L, "episode").contains("## Summary"));
|
||||||
|
// unknown type / stage → empty, never null
|
||||||
|
assertEquals("", service.stageInstruction(1L, "nope", "route"));
|
||||||
|
assertEquals("", service.templateMarkdown(1L, "nope"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void describeForPrompt_customProfile_showsRequiredMetadata() {
|
void describeForPrompt_customProfile_showsRequiredMetadata() {
|
||||||
WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity();
|
WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user