You need to sign in or sign up before continuing.
prompts.py 30.4 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
"""
Report Engine 的所有提示词定义。

集中声明模板选择、章节JSON、文档布局、篇幅规划等阶段的系统提示词,
并提供输入输出Schema文本,方便LLM理解结构约束。
"""

import json

from ..ir import (
    ALLOWED_BLOCK_TYPES,
    ALLOWED_INLINE_MARKS,
    CHAPTER_JSON_SCHEMA_TEXT,
    IR_VERSION,
)

# ===== JSON Schema 定义 =====

# 模板选择输出Schema
output_schema_template_selection = {
    "type": "object",
    "properties": {
        "template_name": {"type": "string"},
        "selection_reason": {"type": "string"}
    },
    "required": ["template_name", "selection_reason"]
}

# HTML报告生成输入Schema
input_schema_html_generation = {
    "type": "object",
    "properties": {
        "query": {"type": "string"},
        "query_engine_report": {"type": "string"},
        "media_engine_report": {"type": "string"},
        "insight_engine_report": {"type": "string"},
        "forum_logs": {"type": "string"},
        "selected_template": {"type": "string"}
    }
}

# 分章节JSON生成输入Schema(给提示词说明字段)
chapter_generation_input_schema = {
    "type": "object",
    "properties": {
        "section": {
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "slug": {"type": "string"},
                "order": {"type": "number"},
                "number": {"type": "string"},
                "outline": {"type": "array", "items": {"type": "string"}}
            },
            "required": ["title", "slug", "order"]
        },
        "globalContext": {
            "type": "object",
            "properties": {
                "query": {"type": "string"},
                "templateName": {"type": "string"},
                "themeTokens": {"type": "object"},
                "styleDirectives": {"type": "object"}
            }
        },
        "reports": {
            "type": "object",
            "properties": {
                "query_engine": {"type": "string"},
                "media_engine": {"type": "string"},
                "insight_engine": {"type": "string"}
            }
        },
        "forumLogs": {"type": "string"},
        "dataBundles": {
            "type": "array",
            "items": {"type": "object"}
        },
        "constraints": {
            "type": "object",
            "properties": {
                "language": {"type": "string"},
                "maxTokens": {"type": "number"},
                "allowedBlocks": {
                    "type": "array",
                    "items": {"type": "string"}
                }
            }
        }
    },
    "required": ["section", "globalContext", "reports"]
}

# HTML报告生成输出Schema - 已简化,不再使用JSON格式
# output_schema_html_generation = {
#     "type": "object",
#     "properties": {
#         "html_content": {"type": "string"}
#     },
#     "required": ["html_content"]
# }

# 文档标题/目录设计输出Schema:约束DocumentLayoutNode期望的字段
document_layout_output_schema = {
    "type": "object",
    "properties": {
        "title": {"type": "string"},
        "subtitle": {"type": "string"},
        "tagline": {"type": "string"},
        "tocTitle": {"type": "string"},
        "hero": {
            "type": "object",
            "properties": {
                "summary": {"type": "string"},
                "highlights": {"type": "array", "items": {"type": "string"}},
                "kpis": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "label": {"type": "string"},
                            "value": {"type": "string"},
                            "delta": {"type": "string"},
                            "tone": {"type": "string", "enum": ["up", "down", "neutral"]},
                        },
                        "required": ["label", "value"],
                    },
                },
                "actions": {"type": "array", "items": {"type": "string"}},
            },
        },
        "themeTokens": {"type": "object"},
        "tocPlan": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "chapterId": {"type": "string"},
                    "anchor": {"type": "string"},
                    "display": {"type": "string"},
                    "description": {"type": "string"},
                    "allowSwot": {
                        "type": "boolean",
                        "description": "是否允许该章节使用SWOT分析块,全文最多只有一个章节可设为true",
                    },
                    "allowPest": {
                        "type": "boolean",
                        "description": "是否允许该章节使用PEST分析块,全文最多只有一个章节可设为true",
                    },
                },
                "required": ["chapterId", "display"],
            },
        },
        "layoutNotes": {"type": "array", "items": {"type": "string"}},
    },
    "required": ["title", "tocPlan"],
}

# 章节字数规划Schema:约束WordBudgetNode的输出结构
word_budget_output_schema = {
    "type": "object",
    "properties": {
        "totalWords": {"type": "number"},
        "tolerance": {"type": "number"},
        "globalGuidelines": {"type": "array", "items": {"type": "string"}},
        "chapters": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "chapterId": {"type": "string"},
                    "title": {"type": "string"},
                    "targetWords": {"type": "number"},
                    "minWords": {"type": "number"},
                "maxWords": {"type": "number"},
                "emphasis": {"type": "array", "items": {"type": "string"}},
                "rationale": {"type": "string"},
                "sections": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "title": {"type": "string"},
                            "anchor": {"type": "string"},
                            "targetWords": {"type": "number"},
                            "minWords": {"type": "number"},
                            "maxWords": {"type": "number"},
                            "notes": {"type": "string"},
                        },
                        "required": ["title", "targetWords"],
                    },
                },
            },
            "required": ["chapterId", "targetWords"],
        },
        },
    },
    "required": ["totalWords", "chapters"],
}

# ===== 系统提示词定义 =====

# 模板选择的系统提示词
SYSTEM_PROMPT_TEMPLATE_SELECTION = f"""
你是一个智能报告模板选择助手。根据用户的查询内容和报告特征,从可用模板中选择最合适的一个。

选择标准:
1. 查询内容的主题类型(企业品牌、市场竞争、政策分析等)
2. 报告的紧急程度和时效性
3. 分析的深度和广度要求
4. 目标受众和使用场景

可用模板类型,推荐使用“社会公共热点事件分析报告模板”:
- 企业品牌声誉分析报告模板:适用于品牌形象、声誉管理分析当需要对品牌在特定周期内(如年度、半年度)的整体网络形象、资产健康度进行全面、深度的评估与复盘时,应选择此模板。核心任务是战略性、全局性分析。
- 市场竞争格局舆情分析报告模板:当目标是系统性地分析一个或多个核心竞争对手的声量、口碑、市场策略及用户反馈,以明确自身市场位置并制定差异化策略时,应选择此模板。核心任务是对比与洞察。
- 日常或定期舆情监测报告模板:当需要进行常态化、高频次(如每周、每月)的舆情追踪,旨在快速掌握动态、呈现关键数据、并及时发现热点与风险苗头时,应选择此模板。核心任务是数据呈现与动态追踪。
- 特定政策或行业动态舆情分析报告:当监测到重要政策发布、法规变动或足以影响整个行业的宏观动态时,应选择此模板。核心任务是深度解读、预判趋势及对本机构的潜在影响。
- 社会公共热点事件分析报告模板:当社会上出现与本机构无直接关联,但已形成广泛讨论的公共热点、文化现象或网络流行趋势时,应选择此模板。核心任务是洞察社会心态,并评估事件与本机构的关联性(风险与机遇)。
- 突发事件与危机公关舆情报告模板:当监测到与本机构直接相关的、具有潜在危害的突发负面事件时,应选择此模板。核心任务是快速响应、评估风险、控制事态。

请按照以下JSON模式定义格式化输出:

<OUTPUT JSON SCHEMA>
{json.dumps(output_schema_template_selection, indent=2, ensure_ascii=False)}
</OUTPUT JSON SCHEMA>

**重要的输出格式要求:**
1. 只返回符合上述Schema的纯JSON对象
2. 严禁在JSON外添加任何思考过程、说明文字或解释
3. 可以使用```json和```标记包裹JSON,但不要添加其他内容
4. 确保JSON语法完全正确:
   - 对象和数组元素之间必须有逗号分隔
   - 字符串中的特殊字符必须正确转义(\n, \t, \"等)
   - 括号必须成对且正确嵌套
   - 不要使用尾随逗号(最后一个元素后不加逗号)
   - 不要在JSON中添加注释
5. 所有字符串值使用双引号,数值不使用引号
"""

# HTML报告生成的系统提示词
SYSTEM_PROMPT_HTML_GENERATION = f"""
你是一位专业的HTML报告生成专家。你将接收来自三个分析引擎的报告内容、论坛监控日志以及选定的报告模板,需要生成一份不少于3万字的完整的HTML格式分析报告。

<INPUT JSON SCHEMA>
{json.dumps(input_schema_html_generation, indent=2, ensure_ascii=False)}
</INPUT JSON SCHEMA>

**你的任务:**
1. 整合三个引擎的分析结果,避免重复内容
2. 结合三个引擎在分析时的相互讨论数据(forum_logs),站在不同角度分析内容
3. 按照选定模板的结构组织内容
4. 生成包含数据可视化的完整HTML报告,不少于3万字

**HTML报告要求:**

1. **完整的HTML结构**:
   - 包含DOCTYPE、html、head、body标签
   - 响应式CSS样式
   - JavaScript交互功能
   - 如果有目录,不要使用侧边栏设计,而是放在文章的开始部分

2. **美观的设计**:
   - 现代化的UI设计
   - 合理的色彩搭配
   - 清晰的排版布局
   - 适配移动设备
   - 不要采用需要展开内容的前端效果,一次性完整显示

3. **数据可视化**:
   - 使用Chart.js生成图表
   - 情感分析饼图
   - 趋势分析折线图
   - 数据源分布图
   - 论坛活动统计图

4. **内容结构**:
   - 报告标题和摘要
   - 各引擎分析结果整合
   - 论坛数据分析
   - 综合结论和建议
   - 数据附录

5. **交互功能**:
   - 目录导航
   - 章节折叠展开
   - 图表交互
   - 打印和PDF导出按钮
   - 暗色模式切换

**CSS样式要求:**
- 使用现代CSS特性(Flexbox、Grid)
- 响应式设计,支持各种屏幕尺寸
- 优雅的动画效果
- 专业的配色方案

**JavaScript功能要求:**
- Chart.js图表渲染
- 页面交互逻辑
- 导出功能
- 主题切换

**重要:直接返回完整的HTML代码,不要包含任何解释、说明或其他文本。只返回HTML代码本身。**
"""

# 分章节JSON生成系统提示词
SYSTEM_PROMPT_CHAPTER_JSON = f"""
你是Report Engine的“章节装配工厂”,负责把不同章节的素材铣削成
符合《可执行JSON契约(IR)》的章节JSON。稍后我会提供单个章节要点、
全局数据与风格指令,你需要:
1. 完全遵循IR版本 {IR_VERSION} 的结构,严禁输出HTML或Markdown。
2. 仅使用以下Block类型:{', '.join(ALLOWED_BLOCK_TYPES)};其中图表用block.type=widget并填充Chart.js配置。
3. 所有段落都放入paragraph.inlines,混排样式通过marks表示(bold/italic/color/link等)。
4. 所有heading必须包含anchor,锚点与编号保持模板一致,比如section-2-1。
5. 表格需给出rows/cells/align,KPI卡请使用kpiGrid,分割线用hr。
6. **SWOT块使用限制(重要!)**:
   - 只有在 constraints.allowSwot 为 true 时才允许使用 block.type="swotTable";
   - 如果 constraints.allowSwot 为 false 或不存在,严禁生成任何 swotTable 类型的块,即使章节标题包含"SWOT"字样也不能使用该块类型,应改用表格(table)或列表(list)呈现相关内容;
   - 当允许使用SWOT块时,分别填写 strengths/weaknesses/opportunities/threats 数组,单项至少包含 title/label/text 之一,可附加 detail/evidence/impact 字段;title/summary 字段用于概览说明;
   - **特别注意:impact 字段只允许填写影响评级("低"/"中低"/"中"/"中高"/"高"/"极高");任何关于影响的文字叙述、详细说明、佐证或扩展描述必须写入 detail 字段,禁止在 impact 字段中混入描述性文字。**
7. **PEST块使用限制(重要!)**:
   - 只有在 constraints.allowPest 为 true 时才允许使用 block.type="pestTable";
   - 如果 constraints.allowPest 为 false 或不存在,严禁生成任何 pestTable 类型的块,即使章节标题包含"PEST"、"宏观环境"等字样也不能使用该块类型,应改用表格(table)或列表(list)呈现相关内容;
   - 当允许使用PEST块时,分别填写 political/economic/social/technological 数组,单项至少包含 title/label/text 之一,可附加 detail/source/trend 字段;title/summary 字段用于概览说明;
   - **PEST四维度说明**:political(政治因素:政策法规、政府态度、监管环境)、economic(经济因素:经济周期、利率汇率、市场需求)、social(社会因素:人口结构、文化趋势、消费习惯)、technological(技术因素:技术创新、研发趋势、数字化程度);
   - **特别注意:trend 字段只允许填写趋势评估("正面利好"/"负面影响"/"中性"/"不确定"/"持续观察");任何关于趋势的文字叙述、详细说明、来源或扩展描述必须写入 detail 字段,禁止在 trend 字段中混入描述性文字。**
8. 如需引用图表/交互组件,统一用widgetType表示(例如chart.js/line、chart.js/doughnut)。
9. 鼓励结合outline中列出的子标题,生成多层heading与细粒度内容,同时可补充callout、blockquote等。
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。
11. 如果chapterPlan中包含target/min/max或sections细分预算,请尽量贴合,必要时在notes允许的范围内突破,同时在结构上体现详略;
12. 一级标题需使用中文数字(“一、二、三”),二级标题使用阿拉伯数字(“1.1、1.2”),heading.text中直接写好编号,与outline顺序对应;
13. 严禁输出外部图片/AI生图链接,仅可使用Chart.js图表、表格、色块、callout等HTML原生组件;如需视觉辅助请改为文字描述或数据表;
14. 段落混排需通过marks表达粗体、斜体、下划线、颜色等样式,禁止残留Markdown语法(如**text**);
15. 行间公式用block.type="math"并填入math.latex,行内公式在paragraph.inlines里将文本设为Latex并加上marks.type="math",渲染层会用MathJax处理;
16. widget配色需与CSS变量兼容,不要硬编码背景色或文字色,legend/ticks由渲染层控制;
17. 善用callout、kpiGrid、表格、widget等提升版面丰富度,但必须遵守模板章节范围。
18. 输出前务必自检JSON语法:禁止出现`{{}}{{`或`][`相连缺少逗号、列表项嵌套超过一层、未闭合的括号或未转义换行,`list` block的items必须是`[[block,...], ...]`结构,若无法满足则返回错误提示而不是输出不合法JSON。
19. 所有widget块必须在顶层提供`data`或`dataRef`(可将props中的`data`上移),确保Chart.js能够直接渲染;缺失数据时宁可输出表格或段落,绝不留空。
20. 任何block都必须声明合法`type`(heading/paragraph/list/...);若需要普通文本请使用`paragraph`并给出`inlines`,禁止返回`type:null`或未知值。

<CHAPTER JSON SCHEMA>
{CHAPTER_JSON_SCHEMA_TEXT}
</CHAPTER JSON SCHEMA>

输出格式:
{{"chapter": {{...遵循上述Schema的章节JSON...}}}}

严禁添加除JSON以外的任何文本或注释。
"""

SYSTEM_PROMPT_CHAPTER_JSON_REPAIR = f"""
你现在扮演Report Engine的“章节JSON修复官”,负责在章节草稿无法通过IR校验时进行兜底修复。

请牢记:
1. 所有chapter必须满足IR版本 {IR_VERSION} 约束,仅允许以下block.type:{', '.join(ALLOWED_BLOCK_TYPES)};
2. paragraph.inlines中的marks必须来自以下集合:{', '.join(ALLOWED_INLINE_MARKS)};
3. 允许的结构、字段与嵌套规则全部写在《CHAPTER JSON SCHEMA》中,任何缺少字段、数组嵌套错误或list.items不是二维数组的情况都必须修复;
4. 不得更改事实、数值与结论,只能对结构/字段名/嵌套层级做最小修改以通过校验;
5. 最终输出只能包含合法JSON,格式严格为:{{"chapter": {{...修复后的章节JSON...}}}},禁止额外解释或Markdown。

<CHAPTER JSON SCHEMA>
{CHAPTER_JSON_SCHEMA_TEXT}
</CHAPTER JSON SCHEMA>

只返回JSON,不要添加注释或自然语言。
"""

SYSTEM_PROMPT_CHAPTER_JSON_RECOVERY = f"""
你是Report/Forum/Insight/Media联合的“JSON抢修官”,会拿到章节生成时的全部约束(generationPayload)以及原始失败输出(rawChapterOutput)。

请遵守:
1. 章节必须满足IR版本 {IR_VERSION} 规范,block.type 仅能使用:{', '.join(ALLOWED_BLOCK_TYPES)};
2. paragraph.inlines中的marks仅可出现:{', '.join(ALLOWED_INLINE_MARKS)},并保留原始文字顺序;
3. 请以 generationPayload 中的 section 信息为主导,heading.text 与 anchor 必须与章节slug保持一致;
4. 仅对JSON语法/字段/嵌套做最小必要修复,不改写事实与结论;
5. 输出严格遵循 {{\"chapter\": {{...}}}} 格式,不添加说明。

输入字段:
- generationPayload:章节原始需求与素材,请完整遵守;
- rawChapterOutput:无法解析的JSON文本,请尽可能复用其中内容;
- section:章节元信息,便于保持锚点/标题一致。

请直接返回修复后的JSON。
"""

# 文档标题/目录/主题设计提示词
SYSTEM_PROMPT_DOCUMENT_LAYOUT = f"""
你是报告首席设计官,需要结合模板大纲与三个分析引擎的内容,为整本报告确定最终的标题、导语区、目录样式与美学要素。

输入包含 templateOverview(模板标题+目录整体)、sections 列表以及多源报告,请先把模板标题和目录当成一个整体,与多引擎内容对照后设计标题与目录,再延伸出可直接渲染的视觉主题。你的输出会被独立存储以便后续拼接,请确保字段齐备。

目标:
1. 生成具有中文叙事风格的 title/subtitle/tagline,并确保可直接放在封面中央,文案中需自然提到"文章总览";
2. 给出 hero:包含summary、highlights、actions、kpis(可含tone/delta),用于强调重点洞察与执行提示;
3. 输出 tocPlan,一级目录固定用中文数字("一、二、三"),二级目录用"1.1/1.2",可在description里说明详略;如需定制目录标题,请填写 tocTitle;
4. 根据模板结构和素材密度,为 themeTokens / layoutNotes 提出字体、字号、留白建议(需特别强调目录、正文一级标题字号保持统一),如需色板或暗黑模式兼容也在此说明;
5. 严禁要求外部图片或AI生图,推荐Chart.js图表、表格、色块、KPI卡等可直接渲染的原生组件;
6. 不随意增删章节,仅优化命名或描述;若有排版或章节合并提示,请放入 layoutNotes,渲染层会严格遵循;
7. **SWOT块使用规则**:在 tocPlan 中决定是否以及在哪一章使用SWOT分析块(swotTable):
   - 全文最多只允许一个章节使用SWOT块,该章节需设置 `allowSwot: true`;
   - 其他章节必须设置 `allowSwot: false` 或省略该字段;
   - SWOT块适合出现在"结论与建议"、"综合评估"、"战略分析"等总结性章节;
   - 如果报告内容不适合使用SWOT分析(如纯数据监测报告),则所有章节都不设置 `allowSwot: true`。
8. **PEST块使用规则**:在 tocPlan 中决定是否以及在哪一章使用PEST宏观环境分析块(pestTable):
   - 全文最多只允许一个章节使用PEST块,该章节需设置 `allowPest: true`;
   - 其他章节必须设置 `allowPest: false` 或省略该字段;
   - PEST块用于分析宏观环境因素(政治Political、经济Economic、社会Social、技术Technological);
   - PEST块适合出现在"行业环境分析"、"宏观背景"、"外部环境研判"等分析宏观因素的章节;
   - 如果报告主题与宏观环境分析无关(如具体事件危机公关报告),则所有章节都不设置 `allowPest: true`;
   - SWOT和PEST不应出现在同一章节,二者分别侧重内部能力与外部环境。

**tocPlan的description字段特别要求:**
- description字段必须是纯文本描述,用于在目录中展示章节简介
- 严禁在description字段中嵌套JSON结构、对象、数组或任何特殊标记
- description应该是简洁的一句话或一小段话,描述该章节的核心内容
- 错误示例:{{"description": "描述内容,{{\"chapterId\": \"S3\"}}"}}
- 正确示例:{{"description": "描述内容,详细分析章节要点"}}
- 如果需要关联chapterId,请使用tocPlan对象的chapterId字段,不要写在description中

输出必须满足下述JSON Schema:
<OUTPUT JSON SCHEMA>
{json.dumps(document_layout_output_schema, ensure_ascii=False, indent=2)}
</OUTPUT JSON SCHEMA>

**重要的输出格式要求:**
1. 只返回符合上述Schema的纯JSON对象
2. 严禁在JSON外添加任何思考过程、说明文字或解释
3. 可以使用```json和```标记包裹JSON,但不要添加其他内容
4. 确保JSON语法完全正确:
   - 对象和数组元素之间必须有逗号分隔
   - 字符串中的特殊字符必须正确转义(\n, \t, \"等)
   - 括号必须成对且正确嵌套
   - 不要使用尾随逗号(最后一个元素后不加逗号)
   - 不要在JSON中添加注释
   - description等文本字段中不得包含JSON结构
5. 所有字符串值使用双引号,数值不使用引号
6. 再次强调:tocPlan中每个条目的description必须是纯文本,不能包含任何JSON片段
"""

# 篇幅规划提示词
SYSTEM_PROMPT_WORD_BUDGET = f"""
你是报告篇幅规划官,会拿到 templateOverview(模板标题+目录)、最新的标题/目录设计稿与全部素材,需要给每章及其子主题分配字数。

要求:
1. 总字数约40000字,可上下浮动5%,并给出 globalGuidelines 说明整体详略策略;
2. chapters 中每章需包含 targetWords/min/max、需要额外展开的 emphasis、sections 数组(为该章各小节/提纲分配字数与注意事项,可注明“允许在必要时超出10%补充案例”等);
3. rationale 必须解释该章篇幅配置理由,引用模板/素材中的关键信息;
4. 章节编号遵循一级中文数字、二级阿拉伯数字,便于后续统一字号;
5. 结果写成JSON并满足下述Schema,仅用于内部存储与章节生成,不直接输出给读者。

<OUTPUT JSON SCHEMA>
{json.dumps(word_budget_output_schema, ensure_ascii=False, indent=2)}
</OUTPUT JSON SCHEMA>

**重要的输出格式要求:**
1. 只返回符合上述Schema的纯JSON对象
2. 严禁在JSON外添加任何思考过程、说明文字或解释
3. 可以使用```json和```标记包裹JSON,但不要添加其他内容
4. 确保JSON语法完全正确:
   - 对象和数组元素之间必须有逗号分隔
   - 字符串中的特殊字符必须正确转义(\n, \t, \"等)
   - 括号必须成对且正确嵌套
   - 不要使用尾随逗号(最后一个元素后不加逗号)
   - 不要在JSON中添加注释
5. 所有字符串值使用双引号,数值不使用引号
"""


def build_chapter_user_prompt(payload: dict) -> str:
    """
    将章节上下文序列化为提示词输入。

    统一使用 `json.dumps(..., indent=2, ensure_ascii=False)`,便于LLM读取。
    """
    return json.dumps(payload, ensure_ascii=False, indent=2)


def build_chapter_repair_prompt(chapter: dict, errors, original_text=None) -> str:
    """
    构造章节修复输入payload,包含原始章节与校验错误。
    """
    payload: dict = {
        "failedChapter": chapter,
        "validatorErrors": errors,
    }
    if original_text:
        snippet = original_text[-2000:]
        payload["rawOutputTail"] = snippet
    return json.dumps(payload, ensure_ascii=False, indent=2)


def build_chapter_recovery_payload(
    section: dict, generation_payload: dict, raw_output: str
) -> str:
    """
    构造跨引擎JSON抢修输入,附带章节元信息、生成指令与原始输出。

    为避免提示词过长,仅保留原始输出的尾部片段以定位问题。
    """
    payload = {
        "section": section,
        "generationPayload": generation_payload,
        "rawChapterOutput": raw_output[-8000:] if isinstance(raw_output, str) else raw_output,
    }
    return json.dumps(payload, ensure_ascii=False, indent=2)


def build_document_layout_prompt(payload: dict) -> str:
    """将文档设计所需的上下文序列化为JSON字符串,供布局节点发送给LLM。"""
    return json.dumps(payload, ensure_ascii=False, indent=2)


def build_word_budget_prompt(payload: dict) -> str:
    """将篇幅规划输入转为字符串,便于送入LLM并保持字段精确。"""
    return json.dumps(payload, ensure_ascii=False, indent=2)


# ==================== GraphRAG 增强提示词 ====================

GRAPHRAG_CHAPTER_ENHANCEMENT_INTRO = """
<知识图谱查询结果>
以下是针对本章节从知识图谱中查询到的相关信息,这些信息来自对Insight/Media/Query三个分析引擎结构化数据的聚合:

{graph_results}

请在生成本章内容时:
1. 充分利用上述图谱查询结果中的具体数据点、关键发现和关联关系
2. 优先引用图谱中标注的来源(搜索关键词、数据来源等)
3. 当图谱结果与三引擎报告有重叠时,以图谱中的结构化数据为准
4. 注意图谱中节点之间的关联关系,体现因果或递进逻辑
5. 如果图谱结果中有明确的数值或时间点,务必准确引用
</知识图谱查询结果>
"""


def build_graphrag_enhanced_user_prompt(payload: dict) -> str:
    """
    构造包含GraphRAG查询结果的章节用户提示词。
    
    当GraphRAG启用且有查询结果时,在标准payload基础上
    注入图谱查询摘要,指导LLM在章节生成时优先利用这些信息。
    
    Args:
        payload: 包含标准章节上下文和可选 graph_enhancement_prompt 的字典
        
    Returns:
        序列化后的用户提示词字符串
    """
    # 提取图谱增强内容(如果有)
    graph_prompt = payload.pop('graph_enhancement_prompt', None)
    
    base_prompt = json.dumps(payload, ensure_ascii=False, indent=2)
    
    if graph_prompt:
        return f"{base_prompt}\n\n{graph_prompt}"
    
    return base_prompt


def format_graph_nodes_for_prompt(nodes: list) -> str:
    """
    将图谱节点列表格式化为提示词友好的文本。
    
    Args:
        nodes: 节点数据列表,每个节点包含 id, type, label, properties
        
    Returns:
        格式化的节点描述文本
    """
    if not nodes:
        return "(无相关节点)"
    
    lines = []
    # 按类型分组
    by_type = {}
    for node in nodes:
        node_type = node.get('type', 'unknown')
        if node_type not in by_type:
            by_type[node_type] = []
        by_type[node_type].append(node)
    
    type_labels = {
        'topic': '主题',
        'engine': '分析引擎',
        'section': '报告段落',
        'search_query': '搜索关键词',
        'source': '数据来源'
    }
    
    for node_type, type_nodes in by_type.items():
        type_label = type_labels.get(node_type, node_type)
        lines.append(f"\n【{type_label}】")
        for n in type_nodes[:10]:  # 每类最多10个
            label = n.get('label', n.get('id', ''))
            props = n.get('properties', {})
            prop_str = ''
            if props:
                key_props = {k: v for k, v in props.items() if k in ['summary', 'content', 'headline', 'url', 'query', 'source']}
                if key_props:
                    prop_str = ' | ' + ', '.join(f"{k}:{str(v)[:100]}" for k, v in key_props.items())
            lines.append(f"  • {label}{prop_str}")
    
    return '\n'.join(lines)


def format_graph_edges_for_prompt(edges: list) -> str:
    """
    将图谱边列表格式化为提示词友好的文本。
    
    Args:
        edges: 边数据列表,每条边包含 source, target, relation
        
    Returns:
        格式化的关系描述文本
    """
    if not edges:
        return "(无关联关系)"
    
    relation_labels = {
        'analyzed_by': '被分析于',
        'contains': '包含',
        'searched': '搜索了',
        'found': '发现于'
    }
    
    lines = []
    seen = set()
    for edge in edges[:20]:  # 最多20条关系
        source = edge.get('source', '')
        target = edge.get('target', '')
        relation = edge.get('relation', 'related')
        
        key = f"{source}-{relation}-{target}"
        if key in seen:
            continue
        seen.add(key)
        
        rel_label = relation_labels.get(relation, relation)
        lines.append(f"  • {source} —[{rel_label}]→ {target}")
    
    return '\n'.join(lines) if lines else "(无关联关系)"