Optimize the Display of Single-Agent Speech Blocks

This commit is contained in:
马一丁
2025-12-04 18:49:36 +08:00
parent af8c8815de
commit a3f49ca4fc
7 changed files with 90 additions and 16 deletions

View File

@@ -11,6 +11,7 @@ from .schema import (
CHAPTER_JSON_SCHEMA_TEXT,
ALLOWED_BLOCK_TYPES,
ALLOWED_INLINE_MARKS,
ENGINE_AGENT_TITLES,
)
from .validator import IRValidator
@@ -20,5 +21,6 @@ __all__ = [
"CHAPTER_JSON_SCHEMA_TEXT",
"ALLOWED_BLOCK_TYPES",
"ALLOWED_INLINE_MARKS",
"ENGINE_AGENT_TITLES",
"IRValidator",
]

View File

@@ -45,6 +45,12 @@ ALLOWED_BLOCK_TYPES: List[str] = [
"toc",
]
ENGINE_AGENT_TITLES: Dict[str, str] = {
"insight": "Insight Agent",
"media": "Media Agent",
"query": "Query Agent",
}
# ====== Schema定义 ======
inline_mark_schema: Dict[str, Any] = {
"type": "object",
@@ -190,7 +196,21 @@ engine_quote_block: Dict[str, Any] = {
"items": {"$ref": "#/definitions/block"},
},
},
"required": ["type", "engine", "blocks"],
"required": ["type", "engine", "blocks", "title"],
"allOf": [
{
"if": {"properties": {"engine": {"const": "insight"}}},
"then": {"properties": {"title": {"const": ENGINE_AGENT_TITLES["insight"]}}},
},
{
"if": {"properties": {"engine": {"const": "media"}}},
"then": {"properties": {"title": {"const": ENGINE_AGENT_TITLES["media"]}}},
},
{
"if": {"properties": {"engine": {"const": "query"}}},
"then": {"properties": {"title": {"const": ENGINE_AGENT_TITLES["query"]}}},
},
],
"additionalProperties": True,
}
@@ -384,4 +404,5 @@ __all__ = [
"ALLOWED_BLOCK_TYPES",
"CHAPTER_JSON_SCHEMA",
"CHAPTER_JSON_SCHEMA_TEXT",
"ENGINE_AGENT_TITLES",
]

View File

@@ -10,7 +10,12 @@ from __future__ import annotations
from typing import Any, Dict, List, Tuple
from .schema import ALLOWED_BLOCK_TYPES, ALLOWED_INLINE_MARKS, IR_VERSION
from .schema import (
ALLOWED_BLOCK_TYPES,
ALLOWED_INLINE_MARKS,
ENGINE_AGENT_TITLES,
IR_VERSION,
)
class IRValidator:
@@ -142,9 +147,20 @@ class IRValidator:
self, block: Dict[str, Any], path: str, errors: List[str]
):
"""单引擎发言块需标注engine并包含子blocks"""
engine = block.get("engine")
engine_raw = block.get("engine")
engine = engine_raw.lower() if isinstance(engine_raw, str) else None
if engine not in {"insight", "media", "query"}:
errors.append(f"{path}.engine 取值非法: {engine}")
errors.append(f"{path}.engine 取值非法: {engine_raw}")
title = block.get("title")
expected_title = ENGINE_AGENT_TITLES.get(engine) if engine else None
if title is None:
errors.append(f"{path}.title 缺失")
elif not isinstance(title, str):
errors.append(f"{path}.title 必须是字符串")
elif expected_title and title != expected_title:
errors.append(
f"{path}.title 必须与engine一致使用对应Agent名称: {expected_title}"
)
inner = block.get("blocks")
if not isinstance(inner, list) or not inner:
errors.append(f"{path}.blocks 必须是非空数组")

View File

@@ -16,7 +16,12 @@ from typing import Any, Dict, List, Tuple, Callable, Optional, Set
from loguru import logger
from ..core import TemplateSection, ChapterStorage
from ..ir import ALLOWED_BLOCK_TYPES, ALLOWED_INLINE_MARKS, IRValidator
from ..ir import (
ALLOWED_BLOCK_TYPES,
ALLOWED_INLINE_MARKS,
ENGINE_AGENT_TITLES,
IRValidator,
)
from ..prompts import (
SYSTEM_PROMPT_CHAPTER_JSON,
SYSTEM_PROMPT_CHAPTER_JSON_REPAIR,
@@ -1081,7 +1086,13 @@ class ChapterGenerationNode(BaseNode):
block["rows"] = rows
def _sanitize_engine_quote_block(self, block: Dict[str, Any]):
"""engineQuote内部仅允许paragraph且仅保留bold/italic样式"""
"""engineQuote仅用于单Agent发言内部仅允许paragraph且title需锁定Agent名称"""
engine_raw = block.get("engine")
engine = engine_raw.lower() if isinstance(engine_raw, str) else None
if engine not in ENGINE_AGENT_TITLES:
engine = "insight"
block["engine"] = engine
block["title"] = ENGINE_AGENT_TITLES[engine]
allowed_marks = {"bold", "italic"}
raw_blocks = block.get("blocks")
candidates = raw_blocks if isinstance(raw_blocks, list) else ([raw_blocks] if raw_blocks else [])

View File

@@ -306,7 +306,7 @@ SYSTEM_PROMPT_CHAPTER_JSON = f"""
5. 表格需给出rows/cells/alignKPI卡请使用kpiGrid分割线用hr。
6. 如需引用图表/交互组件统一用widgetType表示例如chart.js/line、chart.js/doughnut
7. 鼓励结合outline中列出的子标题生成多层heading与细粒度内容同时可补充callout、blockquote等。
8. 如需标注某个引擎的原话,请用 block.type="engineQuote"engine 取值 insight/media/query(仅限这三种),内部 blocks 只允许 paragraphparagraph.inlines 的 marks 仅可使用 bold/italic可留空禁止在 engineQuote 中放表格/图表/引用/公式等。
8. engineQuote 仅用于呈现单Agent的原话:使用 block.type="engineQuote"engine 取值 insight/media/querytitle 必须固定为对应Agent名字insight->Insight Agentmedia->Media Agentquery->Query Agent不可自定义),内部 blocks 只允许 paragraphparagraph.inlines 的 marks 仅可使用 bold/italic可留空禁止在 engineQuote 中放表格/图表/引用/公式等。
9. 如果chapterPlan中包含target/min/max或sections细分预算请尽量贴合必要时在notes允许的范围内突破同时在结构上体现详略
10. 一级标题需使用中文数字“一、二、三”二级标题使用阿拉伯数字“1.1、1.2”heading.text中直接写好编号与outline顺序对应
11. 严禁输出外部图片/AI生图链接仅可使用Chart.js图表、表格、色块、callout等HTML原生组件如需视觉辅助请改为文字描述或数据表

View File

@@ -20,6 +20,7 @@ from pathlib import Path
from typing import Any, Dict, List
from loguru import logger
from ReportEngine.ir.schema import ENGINE_AGENT_TITLES
from ReportEngine.utils.chart_validator import (
ChartValidator,
ChartRepairer,
@@ -1287,15 +1288,10 @@ class HTMLRenderer:
def _render_engine_quote(self, block: Dict[str, Any]) -> str:
"""渲染单Engine发言块带独立配色与标题"""
engine_raw = (block.get("engine") or "").lower()
engine = engine_raw if engine_raw in {"insight", "media", "query"} else "insight"
title = (
block.get("title")
or {
"insight": "Insight Engine 发言",
"media": "Media Engine 发言",
"query": "Query Engine 发言",
}.get(engine, "Engine 发言")
)
engine = engine_raw if engine_raw in ENGINE_AGENT_TITLES else "insight"
expected_title = ENGINE_AGENT_TITLES.get(engine, ENGINE_AGENT_TITLES["insight"])
title_raw = block.get("title") if isinstance(block.get("title"), str) else ""
title = title_raw if title_raw == expected_title else expected_title
inner = self._render_blocks(block.get("blocks", []))
return (
f'<div class="engine-quote engine-{self._escape_attr(engine)}">'

View File

@@ -63,6 +63,7 @@ class ChapterSanitizationTestCase(unittest.TestCase):
{
"type": "engineQuote",
"engine": "insight",
"title": "Insight Agent",
"blocks": [
{
"type": "paragraph",
@@ -87,6 +88,7 @@ class ChapterSanitizationTestCase(unittest.TestCase):
{
"type": "engineQuote",
"engine": "media",
"title": "Media Agent",
"blocks": [
{"type": "math", "latex": "x=y"},
{
@@ -129,6 +131,7 @@ class ChapterSanitizationTestCase(unittest.TestCase):
node._sanitize_chapter_blocks(chapter)
eq_block = chapter["blocks"][0]
self.assertEqual(eq_block["type"], "engineQuote")
self.assertEqual(eq_block.get("title"), "Query Agent")
inner_blocks = eq_block.get("blocks")
self.assertTrue(all(b.get("type") == "paragraph" for b in inner_blocks))
marks = inner_blocks[0]["inlines"][0].get("marks")
@@ -136,6 +139,31 @@ class ChapterSanitizationTestCase(unittest.TestCase):
marks2 = inner_blocks[1]["inlines"][0].get("marks")
self.assertEqual(marks2, [{"type": "bold"}])
def test_engine_quote_title_must_match_engine(self):
validator = IRValidator()
chapter = {
"chapterId": "S1",
"title": "Engine 引用校验",
"anchor": "section-1",
"order": 1,
"blocks": [
{
"type": "engineQuote",
"engine": "query",
"title": "Media Agent",
"blocks": [
{
"type": "paragraph",
"inlines": [{"text": "错误标题"}],
}
],
}
],
}
valid, errors = validator.validate_chapter(chapter)
self.assertFalse(valid)
self.assertTrue(any("title 必须与engine一致" in err for err in errors))
if __name__ == "__main__":
unittest.main()