马一丁

Add support for PEST blocks

@@ -34,6 +34,7 @@ ALLOWED_BLOCK_TYPES: List[str] = [ @@ -34,6 +34,7 @@ ALLOWED_BLOCK_TYPES: List[str] = [
34 "list", 34 "list",
35 "table", 35 "table",
36 "swotTable", 36 "swotTable",
  37 + "pestTable",
37 "blockquote", 38 "blockquote",
38 "engineQuote", 39 "engineQuote",
39 "hr", 40 "hr",
@@ -236,6 +237,71 @@ swot_block: Dict[str, Any] = { @@ -236,6 +237,71 @@ swot_block: Dict[str, Any] = {
236 "additionalProperties": True, 237 "additionalProperties": True,
237 } 238 }
238 239
  240 +pest_item_schema: Dict[str, Any] = {
  241 + "title": "PestItem",
  242 + "oneOf": [
  243 + {"type": "string"},
  244 + {
  245 + "type": "object",
  246 + "properties": {
  247 + "title": {"type": "string"},
  248 + "label": {"type": "string"},
  249 + "text": {"type": "string"},
  250 + "detail": {"type": "string"},
  251 + "description": {"type": "string"},
  252 + "source": {"type": "string"},
  253 + "evidence": {"type": "string"},
  254 + "trend": {
  255 + "type": "string",
  256 + "enum": ["正面利好", "负面影响", "中性", "不确定", "持续观察"],
  257 + "description": "趋势/影响评估,只允许填写:正面利好/负面影响/中性/不确定/持续观察",
  258 + },
  259 + "impact": {"type": ["string", "number"]},
  260 + },
  261 + "required": [],
  262 + "additionalProperties": True,
  263 + },
  264 + ],
  265 +}
  266 +
  267 +pest_block: Dict[str, Any] = {
  268 + "title": "PestTableBlock",
  269 + "type": "object",
  270 + "properties": {
  271 + "type": {"const": "pestTable"},
  272 + "title": {"type": "string"},
  273 + "summary": {"type": "string"},
  274 + "political": {
  275 + "type": "array",
  276 + "items": {"$ref": "#/definitions/pestItem"},
  277 + "description": "政治因素:政策法规、政府态度、政治稳定性等",
  278 + },
  279 + "economic": {
  280 + "type": "array",
  281 + "items": {"$ref": "#/definitions/pestItem"},
  282 + "description": "经济因素:经济周期、利率汇率、消费水平等",
  283 + },
  284 + "social": {
  285 + "type": "array",
  286 + "items": {"$ref": "#/definitions/pestItem"},
  287 + "description": "社会因素:人口结构、文化趋势、生活方式等",
  288 + },
  289 + "technological": {
  290 + "type": "array",
  291 + "items": {"$ref": "#/definitions/pestItem"},
  292 + "description": "技术因素:技术创新、研发投入、技术普及等",
  293 + },
  294 + },
  295 + "required": ["type"],
  296 + "anyOf": [
  297 + {"required": ["political"]},
  298 + {"required": ["economic"]},
  299 + {"required": ["social"]},
  300 + {"required": ["technological"]},
  301 + ],
  302 + "additionalProperties": True,
  303 +}
  304 +
239 blockquote_block: Dict[str, Any] = { 305 blockquote_block: Dict[str, Any] = {
240 "title": "BlockquoteBlock", 306 "title": "BlockquoteBlock",
241 "type": "object", 307 "type": "object",
@@ -429,6 +495,7 @@ block_variants: List[Dict[str, Any]] = [ @@ -429,6 +495,7 @@ block_variants: List[Dict[str, Any]] = [
429 widget_block, 495 widget_block,
430 toc_block, 496 toc_block,
431 swot_block, 497 swot_block,
  498 + pest_block,
432 ] 499 ]
433 500
434 CHAPTER_JSON_SCHEMA: Dict[str, Any] = { 501 CHAPTER_JSON_SCHEMA: Dict[str, Any] = {
@@ -457,6 +524,7 @@ CHAPTER_JSON_SCHEMA: Dict[str, Any] = { @@ -457,6 +524,7 @@ CHAPTER_JSON_SCHEMA: Dict[str, Any] = {
457 "inlineMark": inline_mark_schema, 524 "inlineMark": inline_mark_schema,
458 "inlineRun": inline_run_schema, 525 "inlineRun": inline_run_schema,
459 "swotItem": swot_item_schema, 526 "swotItem": swot_item_schema,
  527 + "pestItem": pest_item_schema,
460 "block": {"oneOf": block_variants}, 528 "block": {"oneOf": block_variants},
461 }, 529 },
462 } 530 }
@@ -307,8 +307,9 @@ class ChapterGenerationNode(BaseNode): @@ -307,8 +307,9 @@ class ChapterGenerationNode(BaseNode):
307 chapter_plan_map = context.get("chapter_directives", {}) 307 chapter_plan_map = context.get("chapter_directives", {})
308 chapter_plan = chapter_plan_map.get(section.chapter_id) if chapter_plan_map else {} 308 chapter_plan = chapter_plan_map.get(section.chapter_id) if chapter_plan_map else {}
309 309
310 - # 从 layout 的 tocPlan 中查找该章节是否允许使用SWOT块 310 + # 从 layout 的 tocPlan 中查找该章节是否允许使用SWOT块和PEST块
311 allow_swot = self._get_chapter_swot_permission(section.chapter_id, context) 311 allow_swot = self._get_chapter_swot_permission(section.chapter_id, context)
  312 + allow_pest = self._get_chapter_pest_permission(section.chapter_id, context)
312 313
313 payload = { 314 payload = {
314 "section": { 315 "section": {
@@ -340,6 +341,7 @@ class ChapterGenerationNode(BaseNode): @@ -340,6 +341,7 @@ class ChapterGenerationNode(BaseNode):
340 "maxTokens": context.get("max_tokens", 4096), 341 "maxTokens": context.get("max_tokens", 4096),
341 "allowedBlocks": ALLOWED_BLOCK_TYPES, 342 "allowedBlocks": ALLOWED_BLOCK_TYPES,
342 "allowSwot": allow_swot, 343 "allowSwot": allow_swot,
  344 + "allowPest": allow_pest,
343 "styleHints": { 345 "styleHints": {
344 "expectWidgets": True, 346 "expectWidgets": True,
345 "forceHeadingAnchors": True, 347 "forceHeadingAnchors": True,
@@ -394,6 +396,42 @@ class ChapterGenerationNode(BaseNode): @@ -394,6 +396,42 @@ class ChapterGenerationNode(BaseNode):
394 396
395 return False 397 return False
396 398
  399 + def _get_chapter_pest_permission(self, chapter_id: str, context: Dict[str, Any]) -> bool:
  400 + """
  401 + 从 layout 的 tocPlan 中查找指定章节是否允许使用 PEST 块。
  402 +
  403 + 全文最多只有一个章节允许使用 PEST 块,由文档设计阶段在 tocPlan 中
  404 + 通过 allowPest 字段标记。
  405 +
  406 + PEST块用于宏观环境分析:
  407 + - Political(政治因素)
  408 + - Economic(经济因素)
  409 + - Social(社会因素)
  410 + - Technological(技术因素)
  411 +
  412 + 参数:
  413 + chapter_id: 当前章节ID。
  414 + context: 全局上下文字典。
  415 +
  416 + 返回:
  417 + bool: 如果该章节允许使用 PEST 块则返回 True,否则返回 False。
  418 + """
  419 + layout = context.get("layout")
  420 + if not isinstance(layout, dict):
  421 + return False
  422 +
  423 + toc_plan = layout.get("tocPlan")
  424 + if not isinstance(toc_plan, list):
  425 + return False
  426 +
  427 + for entry in toc_plan:
  428 + if not isinstance(entry, dict):
  429 + continue
  430 + if entry.get("chapterId") == chapter_id:
  431 + return bool(entry.get("allowPest", False))
  432 +
  433 + return False
  434 +
397 def _stream_llm( 435 def _stream_llm(
398 self, 436 self,
399 user_message: str, 437 user_message: str,
@@ -143,6 +143,10 @@ document_layout_output_schema = { @@ -143,6 +143,10 @@ document_layout_output_schema = {
143 "type": "boolean", 143 "type": "boolean",
144 "description": "是否允许该章节使用SWOT分析块,全文最多只有一个章节可设为true", 144 "description": "是否允许该章节使用SWOT分析块,全文最多只有一个章节可设为true",
145 }, 145 },
  146 + "allowPest": {
  147 + "type": "boolean",
  148 + "description": "是否允许该章节使用PEST分析块,全文最多只有一个章节可设为true",
  149 + },
146 }, 150 },
147 "required": ["chapterId", "display"], 151 "required": ["chapterId", "display"],
148 }, 152 },
@@ -313,19 +317,25 @@ SYSTEM_PROMPT_CHAPTER_JSON = f""" @@ -313,19 +317,25 @@ SYSTEM_PROMPT_CHAPTER_JSON = f"""
313 - 如果 constraints.allowSwot 为 false 或不存在,严禁生成任何 swotTable 类型的块,即使章节标题包含"SWOT"字样也不能使用该块类型,应改用表格(table)或列表(list)呈现相关内容; 317 - 如果 constraints.allowSwot 为 false 或不存在,严禁生成任何 swotTable 类型的块,即使章节标题包含"SWOT"字样也不能使用该块类型,应改用表格(table)或列表(list)呈现相关内容;
314 - 当允许使用SWOT块时,分别填写 strengths/weaknesses/opportunities/threats 数组,单项至少包含 title/label/text 之一,可附加 detail/evidence/impact 字段;title/summary 字段用于概览说明; 318 - 当允许使用SWOT块时,分别填写 strengths/weaknesses/opportunities/threats 数组,单项至少包含 title/label/text 之一,可附加 detail/evidence/impact 字段;title/summary 字段用于概览说明;
315 - **特别注意:impact 字段只允许填写影响评级("低"/"中低"/"中"/"中高"/"高"/"极高");任何关于影响的文字叙述、详细说明、佐证或扩展描述必须写入 detail 字段,禁止在 impact 字段中混入描述性文字。** 319 - **特别注意:impact 字段只允许填写影响评级("低"/"中低"/"中"/"中高"/"高"/"极高");任何关于影响的文字叙述、详细说明、佐证或扩展描述必须写入 detail 字段,禁止在 impact 字段中混入描述性文字。**
316 -7. 如需引用图表/交互组件,统一用widgetType表示(例如chart.js/line、chart.js/doughnut)。  
317 -8. 鼓励结合outline中列出的子标题,生成多层heading与细粒度内容,同时可补充callout、blockquote等。  
318 -9. 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 中放表格/图表/引用/公式等;当 reports 或 forumLogs 中有明确的文字段落、结论、数字/时间等可直接引用时,优先分别从 Query/Media/Insight 三个 Agent 摘出关键原文或文字版数据放入 engineQuote,尽量覆盖三类 Agent 而非只用单一来源,严禁臆造内容或把表格/图表改写进 engineQuote。  
319 -10. 如果chapterPlan中包含target/min/max或sections细分预算,请尽量贴合,必要时在notes允许的范围内突破,同时在结构上体现详略;  
320 -11. 一级标题需使用中文数字(“一、二、三”),二级标题使用阿拉伯数字(“1.1、1.2”),heading.text中直接写好编号,与outline顺序对应;  
321 -12. 严禁输出外部图片/AI生图链接,仅可使用Chart.js图表、表格、色块、callout等HTML原生组件;如需视觉辅助请改为文字描述或数据表;  
322 -13. 段落混排需通过marks表达粗体、斜体、下划线、颜色等样式,禁止残留Markdown语法(如**text**);  
323 -14. 行间公式用block.type="math"并填入math.latex,行内公式在paragraph.inlines里将文本设为Latex并加上marks.type="math",渲染层会用MathJax处理;  
324 -15. widget配色需与CSS变量兼容,不要硬编码背景色或文字色,legend/ticks由渲染层控制;  
325 -16. 善用callout、kpiGrid、表格、widget等提升版面丰富度,但必须遵守模板章节范围。  
326 -17. 输出前务必自检JSON语法:禁止出现`{{}}{{`或`][`相连缺少逗号、列表项嵌套超过一层、未闭合的括号或未转义换行,`list` block的items必须是`[[block,...], ...]`结构,若无法满足则返回错误提示而不是输出不合法JSON。  
327 -18. 所有widget块必须在顶层提供`data`或`dataRef`(可将props中的`data`上移),确保Chart.js能够直接渲染;缺失数据时宁可输出表格或段落,绝不留空。  
328 -19. 任何block都必须声明合法`type`(heading/paragraph/list/...);若需要普通文本请使用`paragraph`并给出`inlines`,禁止返回`type:null`或未知值。 320 +7. **PEST块使用限制(重要!)**:
  321 + - 只有在 constraints.allowPest 为 true 时才允许使用 block.type="pestTable";
  322 + - 如果 constraints.allowPest 为 false 或不存在,严禁生成任何 pestTable 类型的块,即使章节标题包含"PEST"、"宏观环境"等字样也不能使用该块类型,应改用表格(table)或列表(list)呈现相关内容;
  323 + - 当允许使用PEST块时,分别填写 political/economic/social/technological 数组,单项至少包含 title/label/text 之一,可附加 detail/source/trend 字段;title/summary 字段用于概览说明;
  324 + - **PEST四维度说明**:political(政治因素:政策法规、政府态度、监管环境)、economic(经济因素:经济周期、利率汇率、市场需求)、social(社会因素:人口结构、文化趋势、消费习惯)、technological(技术因素:技术创新、研发趋势、数字化程度);
  325 + - **特别注意:trend 字段只允许填写趋势评估("正面利好"/"负面影响"/"中性"/"不确定"/"持续观察");任何关于趋势的文字叙述、详细说明、来源或扩展描述必须写入 detail 字段,禁止在 trend 字段中混入描述性文字。**
  326 +8. 如需引用图表/交互组件,统一用widgetType表示(例如chart.js/line、chart.js/doughnut)。
  327 +9. 鼓励结合outline中列出的子标题,生成多层heading与细粒度内容,同时可补充callout、blockquote等。
  328 +10. 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 中放表格/图表/引用/公式等;当 reports 或 forumLogs 中有明确的文字段落、结论、数字/时间等可直接引用时,优先分别从 Query/Media/Insight 三个 Agent 摘出关键原文或文字版数据放入 engineQuote,尽量覆盖三类 Agent 而非只用单一来源,严禁臆造内容或把表格/图表改写进 engineQuote。
  329 +11. 如果chapterPlan中包含target/min/max或sections细分预算,请尽量贴合,必要时在notes允许的范围内突破,同时在结构上体现详略;
  330 +12. 一级标题需使用中文数字(“一、二、三”),二级标题使用阿拉伯数字(“1.1、1.2”),heading.text中直接写好编号,与outline顺序对应;
  331 +13. 严禁输出外部图片/AI生图链接,仅可使用Chart.js图表、表格、色块、callout等HTML原生组件;如需视觉辅助请改为文字描述或数据表;
  332 +14. 段落混排需通过marks表达粗体、斜体、下划线、颜色等样式,禁止残留Markdown语法(如**text**);
  333 +15. 行间公式用block.type="math"并填入math.latex,行内公式在paragraph.inlines里将文本设为Latex并加上marks.type="math",渲染层会用MathJax处理;
  334 +16. widget配色需与CSS变量兼容,不要硬编码背景色或文字色,legend/ticks由渲染层控制;
  335 +17. 善用callout、kpiGrid、表格、widget等提升版面丰富度,但必须遵守模板章节范围。
  336 +18. 输出前务必自检JSON语法:禁止出现`{{}}{{`或`][`相连缺少逗号、列表项嵌套超过一层、未闭合的括号或未转义换行,`list` block的items必须是`[[block,...], ...]`结构,若无法满足则返回错误提示而不是输出不合法JSON。
  337 +19. 所有widget块必须在顶层提供`data`或`dataRef`(可将props中的`data`上移),确保Chart.js能够直接渲染;缺失数据时宁可输出表格或段落,绝不留空。
  338 +20. 任何block都必须声明合法`type`(heading/paragraph/list/...);若需要普通文本请使用`paragraph`并给出`inlines`,禁止返回`type:null`或未知值。
329 339
330 <CHAPTER JSON SCHEMA> 340 <CHAPTER JSON SCHEMA>
331 {CHAPTER_JSON_SCHEMA_TEXT} 341 {CHAPTER_JSON_SCHEMA_TEXT}
@@ -390,6 +400,13 @@ SYSTEM_PROMPT_DOCUMENT_LAYOUT = f""" @@ -390,6 +400,13 @@ SYSTEM_PROMPT_DOCUMENT_LAYOUT = f"""
390 - 其他章节必须设置 `allowSwot: false` 或省略该字段; 400 - 其他章节必须设置 `allowSwot: false` 或省略该字段;
391 - SWOT块适合出现在"结论与建议"、"综合评估"、"战略分析"等总结性章节; 401 - SWOT块适合出现在"结论与建议"、"综合评估"、"战略分析"等总结性章节;
392 - 如果报告内容不适合使用SWOT分析(如纯数据监测报告),则所有章节都不设置 `allowSwot: true`。 402 - 如果报告内容不适合使用SWOT分析(如纯数据监测报告),则所有章节都不设置 `allowSwot: true`。
  403 +8. **PEST块使用规则**:在 tocPlan 中决定是否以及在哪一章使用PEST宏观环境分析块(pestTable):
  404 + - 全文最多只允许一个章节使用PEST块,该章节需设置 `allowPest: true`;
  405 + - 其他章节必须设置 `allowPest: false` 或省略该字段;
  406 + - PEST块用于分析宏观环境因素(政治Political、经济Economic、社会Social、技术Technological);
  407 + - PEST块适合出现在"行业环境分析"、"宏观背景"、"外部环境研判"等分析宏观因素的章节;
  408 + - 如果报告主题与宏观环境分析无关(如具体事件危机公关报告),则所有章节都不设置 `allowPest: true`;
  409 + - SWOT和PEST不应出现在同一章节,二者分别侧重内部能力与外部环境。
393 410
394 **tocPlan的description字段特别要求:** 411 **tocPlan的description字段特别要求:**
395 - description字段必须是纯文本描述,用于在目录中展示章节简介 412 - description字段必须是纯文本描述,用于在目录中展示章节简介