builders.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. from __future__ import annotations
  2. import json
  3. from decimal import Decimal
  4. from pathlib import Path
  5. from jinja2 import Environment, FileSystemLoader, select_autoescape
  6. from finrep_algo_agent.schemas.outline import OutlineL1Request, OutlineL2Request
  7. from finrep_algo_agent.schemas.section import SectionRequest
  8. _TPL_DIR = Path(__file__).resolve().parent / "templates"
  9. def _env() -> Environment:
  10. return Environment(
  11. loader=FileSystemLoader(_TPL_DIR),
  12. autoescape=select_autoescape(disabled_extensions=("j2",)),
  13. trim_blocks=True,
  14. lstrip_blocks=True,
  15. )
  16. def _fmt_list(items: list[str] | None) -> str:
  17. if not items:
  18. return "无"
  19. return "、".join(str(x) for x in items)
  20. def _fmt_bool(v: bool | None) -> str:
  21. if v is True:
  22. return "是"
  23. if v is False:
  24. return "否"
  25. return "未提供"
  26. def _fmt_decimal(v: Decimal | None) -> str:
  27. if v is None:
  28. return "未提供"
  29. return str(v)
  30. def _format_kv_block(obj: dict[str, object], indent: str = " ") -> str:
  31. if not obj:
  32. return f"{indent}(无扩展字段)"
  33. lines: list[str] = []
  34. for k in sorted(obj.keys(), key=lambda x: str(x)):
  35. v = obj[k]
  36. if isinstance(v, (dict, list)):
  37. lines.append(f"{indent}{k}: {json.dumps(v, ensure_ascii=False)}")
  38. else:
  39. lines.append(f"{indent}{k}: {v}")
  40. return "\n".join(lines)
  41. def format_chapter_candidates_block(req: OutlineL1Request) -> str:
  42. """仅用于请求体显式传入 `chapter_candidates` 时的覆盖文本。"""
  43. cands = req.chapter_candidates or []
  44. if not cands:
  45. return ""
  46. parts: list[str] = []
  47. for i, c in enumerate(cands, start=1):
  48. row = c.model_dump(mode="python", exclude_none=True)
  49. cid = row.pop("chapter_id", "")
  50. cname = row.pop("chapter_name", "")
  51. parts.append(f"【候选 {i}】")
  52. parts.append(f" chapter_id: {cid}")
  53. parts.append(f" chapter_name: {cname}")
  54. parts.append(" 附加属性(来自预置知识体系,原样供判断):")
  55. parts.append(_format_kv_block(row) if row else " (无扩展字段)")
  56. parts.append("")
  57. return "\n".join(parts).strip()
  58. def format_leaf_chapter_candidates_block(leaf: list[dict]) -> str:
  59. """仅用于请求体显式传入 `leaf_chapter_candidates` 时的覆盖文本。"""
  60. if not leaf:
  61. return ""
  62. parts: list[str] = []
  63. for i, item in enumerate(leaf, start=1):
  64. parts.append(f"【末级候选 {i}】")
  65. parts.append(_format_kv_block(dict(item), indent=" "))
  66. parts.append("")
  67. parts.append("--- JSON(机器可读备份,与上一致)---")
  68. parts.append(json.dumps(leaf, ensure_ascii=False, indent=2))
  69. return "\n".join(parts).strip()
  70. def _task_background_dict(req: OutlineL1Request) -> dict[str, str]:
  71. return {
  72. "report_type": (req.report_type or "").strip(),
  73. "agreement_amount": _fmt_decimal(req.agreement_amount),
  74. "enterprise_type": req.enterprise_type or "未提供",
  75. "group_business_segments": _fmt_list(req.group_business_segments),
  76. "industry_type": req.industry_type or "未提供",
  77. "has_independent_report": _fmt_bool(req.has_independent_report),
  78. "independent_report_types": _fmt_list(req.independent_report_types),
  79. "candidate_financing_tools": _fmt_list(req.candidate_financing_tools),
  80. "recommended_financing_tools": _fmt_list(req.recommended_financing_tools),
  81. "other_requirements": req.other_requirements or "无",
  82. }
  83. def build_outline_l1_user_prompt(req: OutlineL1Request) -> str:
  84. ctx = _task_background_dict(req)
  85. ctx["chapter_candidates_override"] = format_chapter_candidates_block(req)
  86. return _env().get_template("outline_l1.j2").render(**ctx)
  87. def _outline_l2_background(req: OutlineL2Request) -> OutlineL1Request:
  88. if req.l1_task_snapshot is not None:
  89. return req.l1_task_snapshot
  90. return OutlineL1Request(
  91. report_type=req.report_type or "未分类",
  92. agreement_amount=req.agreement_amount,
  93. enterprise_type=req.enterprise_type,
  94. group_business_segments=list(req.group_business_segments),
  95. industry_type=req.industry_type,
  96. has_independent_report=req.has_independent_report,
  97. independent_report_types=list(req.independent_report_types),
  98. candidate_financing_tools=list(req.candidate_financing_tools),
  99. recommended_financing_tools=list(req.recommended_financing_tools),
  100. other_requirements=req.other_requirements,
  101. chapter_candidates=[],
  102. )
  103. def build_outline_l2_user_prompt(req: OutlineL2Request) -> str:
  104. ctx = _task_background_dict(_outline_l2_background(req))
  105. ctx.update(
  106. {
  107. "chapter_name": req.chapter_name,
  108. "chapter_no": req.chapter_no,
  109. "l1_chapter_id": (req.l1_chapter_id or "").strip(),
  110. "chapter_presentation_enum": (req.chapter_presentation_enum or "未提供").strip(),
  111. "chapter_paragraph_count_enum": req.chapter_paragraph_count_enum or "未提供",
  112. "chapter_reason": req.chapter_reason or "无",
  113. "overall_logic": req.overall_logic or "无",
  114. "leaf_chapter_candidates_override": format_leaf_chapter_candidates_block(
  115. req.leaf_chapter_candidates
  116. ),
  117. }
  118. )
  119. return _env().get_template("outline_l2.j2").render(**ctx)
  120. def _format_json_block(data: dict) -> str:
  121. if not data:
  122. return "(无)"
  123. return json.dumps(data, ensure_ascii=False, indent=2)
  124. def build_section_user_prompt(req: SectionRequest) -> str:
  125. tt = (req.template_type or "info").lower()
  126. name_map = {
  127. "info": "section_info.j2",
  128. "analysis": "section_analysis.j2",
  129. "metric": "section_metric.j2",
  130. "judgment": "section_judgment.j2",
  131. }
  132. tpl_name = name_map.get(tt, "section_info.j2")
  133. ctx = {
  134. "report_type": req.report_type or "财务顾问",
  135. "overall_logic": req.overall_logic or "(未提供整篇报告撰写逻辑,请结合输入自行保持语气一致。)",
  136. "chapter_logic": req.chapter_logic or "(未提供一级章节写作逻辑。)",
  137. "paragraph_position": req.paragraph_position or "(未提供段落定位说明。)",
  138. "task_input_block": _format_json_block(req.task_input),
  139. "data_block": _format_json_block(req.data_package),
  140. "paragraph_logic": req.paragraph_logic or "(未提供段落撰写逻辑。)",
  141. "example_block": req.example or "(无示例)",
  142. "notes_block": req.notes or "(无补充注意事项。)",
  143. }
  144. return _env().get_template(tpl_name).render(**ctx)