Optimize the Display of Single-Agent Speech Blocks
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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 必须是非空数组")
|
||||
|
||||
@@ -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 [])
|
||||
|
||||
@@ -306,7 +306,7 @@ SYSTEM_PROMPT_CHAPTER_JSON = f"""
|
||||
5. 表格需给出rows/cells/align,KPI卡请使用kpiGrid,分割线用hr。
|
||||
6. 如需引用图表/交互组件,统一用widgetType表示(例如chart.js/line、chart.js/doughnut)。
|
||||
7. 鼓励结合outline中列出的子标题,生成多层heading与细粒度内容,同时可补充callout、blockquote等。
|
||||
8. 如需标注某个引擎的原话,请用 block.type="engineQuote",engine 取值 insight/media/query(仅限这三种),内部 blocks 只允许 paragraph,paragraph.inlines 的 marks 仅可使用 bold/italic(可留空),禁止在 engineQuote 中放表格/图表/引用/公式等。
|
||||
8. engineQuote 仅用于呈现单Agent的原话:使用 block.type="engineQuote",engine 取值 insight/media/query,title 必须固定为对应Agent名字(insight->Insight Agent,media->Media Agent,query->Query Agent,不可自定义),内部 blocks 只允许 paragraph,paragraph.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原生组件;如需视觉辅助请改为文字描述或数据表;
|
||||
|
||||
@@ -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)}">'
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user