Showing
3 changed files
with
137 additions
and
14 deletions
| @@ -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字段必须是纯文本描述,用于在目录中展示章节简介 |
-
Please register or login to post a comment