build_agent_brief_doc.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. from __future__ import annotations
  2. from pathlib import Path
  3. from docx import Document
  4. from docx.enum.section import WD_SECTION_START
  5. from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
  6. from docx.enum.text import WD_ALIGN_PARAGRAPH
  7. from docx.oxml import OxmlElement
  8. from docx.oxml.ns import qn
  9. from docx.shared import Inches, Pt, RGBColor
  10. ROOT = Path(__file__).resolve().parents[1]
  11. OUT = ROOT / "docs" / "市民请集合智能助手Agent简版建设方案.docx"
  12. BLUE = "2E74B5"
  13. DARK_BLUE = "1F4D78"
  14. INK = "0B2545"
  15. MUTED = "667085"
  16. LIGHT_FILL = "F2F4F7"
  17. CALLOUT_FILL = "F4F6F9"
  18. BORDER = "D0D5DD"
  19. def set_cell_shading(cell, fill: str) -> None:
  20. tc_pr = cell._tc.get_or_add_tcPr()
  21. shd = tc_pr.find(qn("w:shd"))
  22. if shd is None:
  23. shd = OxmlElement("w:shd")
  24. tc_pr.append(shd)
  25. shd.set(qn("w:fill"), fill)
  26. def set_cell_margins(cell, top=80, bottom=80, start=120, end=120) -> None:
  27. tc_pr = cell._tc.get_or_add_tcPr()
  28. tc_mar = tc_pr.first_child_found_in("w:tcMar")
  29. if tc_mar is None:
  30. tc_mar = OxmlElement("w:tcMar")
  31. tc_pr.append(tc_mar)
  32. for edge, value in (("top", top), ("bottom", bottom), ("start", start), ("end", end)):
  33. node = tc_mar.find(qn(f"w:{edge}"))
  34. if node is None:
  35. node = OxmlElement(f"w:{edge}")
  36. tc_mar.append(node)
  37. node.set(qn("w:w"), str(value))
  38. node.set(qn("w:type"), "dxa")
  39. def set_table_width(table, widths_dxa: list[int]) -> None:
  40. tbl_pr = table._tbl.tblPr
  41. tbl_w = tbl_pr.find(qn("w:tblW"))
  42. if tbl_w is None:
  43. tbl_w = OxmlElement("w:tblW")
  44. tbl_pr.append(tbl_w)
  45. tbl_w.set(qn("w:w"), str(sum(widths_dxa)))
  46. tbl_w.set(qn("w:type"), "dxa")
  47. tbl_ind = tbl_pr.find(qn("w:tblInd"))
  48. if tbl_ind is None:
  49. tbl_ind = OxmlElement("w:tblInd")
  50. tbl_pr.append(tbl_ind)
  51. tbl_ind.set(qn("w:w"), "120")
  52. tbl_ind.set(qn("w:type"), "dxa")
  53. grid = table._tbl.tblGrid
  54. if grid is None:
  55. grid = OxmlElement("w:tblGrid")
  56. table._tbl.insert(0, grid)
  57. for child in list(grid):
  58. grid.remove(child)
  59. for width in widths_dxa:
  60. col = OxmlElement("w:gridCol")
  61. col.set(qn("w:w"), str(width))
  62. grid.append(col)
  63. for row in table.rows:
  64. for cell, width in zip(row.cells, widths_dxa):
  65. tc_pr = cell._tc.get_or_add_tcPr()
  66. tc_w = tc_pr.find(qn("w:tcW"))
  67. if tc_w is None:
  68. tc_w = OxmlElement("w:tcW")
  69. tc_pr.append(tc_w)
  70. tc_w.set(qn("w:w"), str(width))
  71. tc_w.set(qn("w:type"), "dxa")
  72. cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
  73. set_cell_margins(cell)
  74. def set_east_asia_font(run, font_name="Microsoft YaHei") -> None:
  75. run.font.name = font_name
  76. run._element.rPr.rFonts.set(qn("w:eastAsia"), font_name)
  77. def add_para(doc, text: str, style: str | None = None, bold_prefix: str | None = None):
  78. p = doc.add_paragraph(style=style)
  79. if bold_prefix and text.startswith(bold_prefix):
  80. r = p.add_run(bold_prefix)
  81. r.bold = True
  82. set_east_asia_font(r)
  83. tail = p.add_run(text[len(bold_prefix):])
  84. set_east_asia_font(tail)
  85. else:
  86. r = p.add_run(text)
  87. set_east_asia_font(r)
  88. return p
  89. def add_bullet(doc, text: str):
  90. p = doc.add_paragraph(style="List Bullet")
  91. r = p.add_run(text)
  92. set_east_asia_font(r)
  93. return p
  94. def add_number(doc, text: str):
  95. p = doc.add_paragraph(style="List Number")
  96. r = p.add_run(text)
  97. set_east_asia_font(r)
  98. return p
  99. def add_heading(doc, text: str, level: int):
  100. p = doc.add_heading(text, level=level)
  101. for run in p.runs:
  102. set_east_asia_font(run)
  103. return p
  104. def add_callout(doc, title: str, body: str):
  105. table = doc.add_table(rows=1, cols=1)
  106. table.alignment = WD_TABLE_ALIGNMENT.LEFT
  107. table.style = "Table Grid"
  108. set_table_width(table, [9360])
  109. cell = table.cell(0, 0)
  110. set_cell_shading(cell, CALLOUT_FILL)
  111. p = cell.paragraphs[0]
  112. r = p.add_run(title)
  113. r.bold = True
  114. r.font.color.rgb = RGBColor.from_string(DARK_BLUE)
  115. set_east_asia_font(r)
  116. p2 = cell.add_paragraph()
  117. r2 = p2.add_run(body)
  118. set_east_asia_font(r2)
  119. doc.add_paragraph()
  120. def add_table(doc, headers: list[str], rows: list[list[str]], widths_dxa: list[int]):
  121. table = doc.add_table(rows=1, cols=len(headers))
  122. table.alignment = WD_TABLE_ALIGNMENT.LEFT
  123. table.style = "Table Grid"
  124. set_table_width(table, widths_dxa)
  125. hdr = table.rows[0]
  126. for i, text in enumerate(headers):
  127. cell = hdr.cells[i]
  128. set_cell_shading(cell, LIGHT_FILL)
  129. p = cell.paragraphs[0]
  130. p.alignment = WD_ALIGN_PARAGRAPH.CENTER
  131. r = p.add_run(text)
  132. r.bold = True
  133. r.font.color.rgb = RGBColor.from_string(INK)
  134. set_east_asia_font(r)
  135. for row in rows:
  136. cells = table.add_row().cells
  137. for i, text in enumerate(row):
  138. p = cells[i].paragraphs[0]
  139. p.alignment = WD_ALIGN_PARAGRAPH.LEFT
  140. r = p.add_run(text)
  141. set_east_asia_font(r)
  142. set_table_width(table, widths_dxa)
  143. doc.add_paragraph()
  144. return table
  145. def configure_styles(doc: Document) -> None:
  146. section = doc.sections[0]
  147. section.page_width = Inches(8.5)
  148. section.page_height = Inches(11)
  149. for margin in ("top_margin", "bottom_margin", "left_margin", "right_margin"):
  150. setattr(section, margin, Inches(1))
  151. section.header_distance = Inches(0.492)
  152. section.footer_distance = Inches(0.492)
  153. styles = doc.styles
  154. normal = styles["Normal"]
  155. normal.font.name = "Calibri"
  156. normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
  157. normal.font.size = Pt(11)
  158. normal.paragraph_format.space_after = Pt(6)
  159. normal.paragraph_format.line_spacing = 1.10
  160. title = styles["Title"]
  161. title.font.name = "Calibri"
  162. title._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
  163. title.font.size = Pt(22)
  164. title.font.bold = True
  165. title.font.color.rgb = RGBColor.from_string(INK)
  166. title.paragraph_format.space_after = Pt(8)
  167. subtitle = styles["Subtitle"]
  168. subtitle.font.name = "Calibri"
  169. subtitle._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
  170. subtitle.font.size = Pt(11)
  171. subtitle.font.color.rgb = RGBColor.from_string(MUTED)
  172. subtitle.paragraph_format.space_after = Pt(14)
  173. for name, size, color, before, after in [
  174. ("Heading 1", 16, BLUE, 16, 8),
  175. ("Heading 2", 13, BLUE, 12, 6),
  176. ("Heading 3", 12, DARK_BLUE, 8, 4),
  177. ]:
  178. style = styles[name]
  179. style.font.name = "Calibri"
  180. style._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
  181. style.font.size = Pt(size)
  182. style.font.bold = True
  183. style.font.color.rgb = RGBColor.from_string(color)
  184. style.paragraph_format.space_before = Pt(before)
  185. style.paragraph_format.space_after = Pt(after)
  186. for name in ("List Bullet", "List Number"):
  187. style = styles[name]
  188. style.font.name = "Calibri"
  189. style._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei")
  190. style.font.size = Pt(11)
  191. style.paragraph_format.space_after = Pt(8)
  192. style.paragraph_format.line_spacing = 1.167
  193. footer = section.footer.paragraphs[0]
  194. footer.alignment = WD_ALIGN_PARAGRAPH.RIGHT
  195. r = footer.add_run("市民请集合智能助手 Agent 简版建设方案")
  196. r.font.size = Pt(9)
  197. r.font.color.rgb = RGBColor.from_string(MUTED)
  198. set_east_asia_font(r)
  199. def build_doc() -> None:
  200. doc = Document()
  201. configure_styles(doc)
  202. p = doc.add_paragraph(style="Title")
  203. r = p.add_run("市民请集合智能助手 Agent 简版建设方案")
  204. set_east_asia_font(r)
  205. p = doc.add_paragraph(style="Subtitle")
  206. r = p.add_run("结合当前 smqjh-agent 项目、Agent 设计复盘方案与结算需求文档整理")
  207. set_east_asia_font(r)
  208. add_para(doc, "版本:讨论稿 v0.1 日期:2026-06-08 适用:内部产品/研发/运营沟通")
  209. add_callout(
  210. doc,
  211. "核心判断",
  212. "这个产品不应只是一个聊天框,而应逐步升级为“模型自主规划、MCP 统一工具、业务系统只读查询、结果表格化/文件化、关键动作人工确认”的内部业务 Agent。"
  213. )
  214. add_heading(doc, "1. 项目定位", 1)
  215. add_para(doc, "市民请集合智能助手定位为面向后台管理员和运营人员的桌面端业务 Agent。第一版先解决“查数据、看依据、生成表格、辅助结算、记录人工确认事项”,后续再扩展到充值、消费履约、发券、对账、运维巡检等流程。")
  216. add_bullet(doc, "不是通用聊天机器人:回答范围限定在 smqjh 项目业务和日常简单问答。")
  217. add_bullet(doc, "不是简单任务按钮集合:模型应根据用户目标判断是否需要查询数据库、调用接口、生成文件或进入人工确认。")
  218. add_bullet(doc, "不是自动写入业务系统:涉及结算确认、充值导入、发券、扣减等动作必须进入人工确认。")
  219. add_heading(doc, "2. 当前项目已具备的底座", 1)
  220. add_table(
  221. doc,
  222. ["能力", "当前状态", "下一步意义"],
  223. [
  224. ["桌面端界面", "Electron 已搭建,包含对话、任务、配置、日志。", "继续作为管理员统一入口,重点优化过程展示和文件入口。"],
  225. ["MCP 服务", "Java MCP 已接管 DeepSeek、数据库配置和工具注册。", "后续把更多业务工具迁到 MCP,桌面端少做业务判断。"],
  226. ["只读数据库", "已支持 SELECT 校验、白名单表、默认 LIMIT、超时和只读事务。", "业务事实查询默认由 Agent 自动完成,不让用户自己执行 SQL。"],
  227. ["智能 SQL 查询", "已新增 smart query,由 DeepSeek 生成 SQL,MCP 安全执行并返回表格。", "解决“识别到了但不执行”的关键问题。"],
  228. ["Excel 产物", "已支持通用表格导出和价格对比导出。", "用户说“做成表格/导出 Excel”时可以直接产出文件。"],
  229. ["结算雏形", "已建立月结企业清单和企业月结处理计划工具。", "作为第一个明确业务场景继续深化。"],
  230. ],
  231. [1700, 3650, 4010],
  232. )
  233. add_heading(doc, "3. 推荐目标架构", 1)
  234. add_number(doc, "桌面端:只负责登录、对话、结果展示、文件打开、人工确认和日志查看。")
  235. add_number(doc, "Agent Runtime:负责多步循环,按“规划 -> 调工具 -> 观察 -> 再规划 -> 总结”的方式完成任务。")
  236. add_number(doc, "Java MCP:统一暴露业务工具、schema、业务口径、DeepSeek 调用、数据库只读查询和后续文件产物工具。")
  237. add_number(doc, "业务系统/数据库:提供只读查询和导出接口;写入类动作必须经过人工确认。")
  238. add_number(doc, "审计与评测:记录用户问题、工具调用、SQL、结果行数、耗时、失败原因,形成可回放链路。")
  239. add_heading(doc, "4. 一期建议功能清单", 1)
  240. add_table(
  241. doc,
  242. ["模块", "一期功能", "验收口径"],
  243. [
  244. ["对话工作台", "自然语言输入、多轮上下文、表格结果展示。", "用户问商品、订单、会员、物流时,Agent 能自动查并直接回答。"],
  245. ["工具调用", "MCP 工具规划、智能 SQL、商品查询、订单统计、月结计划。", "工具调用过程有日志,有失败提示,不再让用户自己查后台。"],
  246. ["文件产物", "把任意表格结果导出 Excel,后续扩展为结算报告。", "上一轮表格可直接导出并打开。"],
  247. ["人工确认", "差异、金额、写入、发券、充值导入统一标记人工确认。", "Agent 只给建议和待确认项,不擅自改业务系统。"],
  248. ["配置与权限", "DeepSeek、数据库、MCP 配置集中在 MCP;顶级 admin 不强制租户 ID。", "配置可持久化,重启后不反复输入。"],
  249. ["日志追踪", "记录工具、SQL、导出文件、异常和耗时。", "出现错误时能定位是模型、SQL、数据库还是接口问题。"],
  250. ],
  251. [1700, 4300, 3860],
  252. )
  253. add_heading(doc, "5. 第一个业务需求:结算、充值、消费", 1)
  254. add_para(doc, "结算需求来自你提供的《结算.docx》,目前可拆成三个业务场景:")
  255. add_heading(doc, "5.1 结算流程", 2)
  256. add_number(doc, "平台生成结算报告,包含产品订单/商品费用、运费、积分抵扣、现金支付等数据。")
  257. add_number(doc, "补齐海博编码、商品编码、SKU 编码等业务字段。")
  258. add_number(doc, "价格浮动或金额差异时只标记“需人工确认”,不自动修改系统。")
  259. add_number(doc, "发送客户核对,之后打印盖章、开具电子发票、等待客户付款。")
  260. add_heading(doc, "5.2 充值流程", 2)
  261. add_number(doc, "从企业获取客户充值信息表。")
  262. add_number(doc, "Agent 辅助排错、匹配企业/员工/手机号/金额。")
  263. add_number(doc, "人工确认后导入业务系统。")
  264. add_number(doc, "生成上账成功凭据和上账记录。")
  265. add_heading(doc, "5.3 消费履约流程", 2)
  266. add_number(doc, "C 端用户即时发货配送。")
  267. add_number(doc, "企业用户统一发货,非及时性发货需要导出订单并按仓库分配。")
  268. add_number(doc, "联系供应商配合入库,再选择闪送、物流快递等配送方式。")
  269. add_heading(doc, "6. 结算 Agent 功能拆解", 1)
  270. add_table(
  271. doc,
  272. ["子能力", "Agent 需要做的事", "输出产物", "人工确认"],
  273. [
  274. ["月结企业识别", "识别铜仁移动、招商银行贵阳分行、中数未来等月结企业,匹配 channelId/channelNo。", "月结企业清单。", "企业范围变化时确认。"],
  275. ["月份选择", "由管理员选择月份,自动计算开始/结束时间。", "结算月份和筛选条件。", "跨月、补单时确认。"],
  276. ["数据导出", "调用员工列表、订单表、商品订单、运费账单、对账汇总模板等接口。", "原始导出表。", "接口失败或字段缺失时确认。"],
  277. ["数据补齐", "从商品表补齐商品编码、SKU、海博编码等。", "补齐后的订单/商品明细。", "商品匹配不唯一时确认。"],
  278. ["汇总核对", "计算商品总额、运费、积分抵扣、现金支付、差异。", "结算汇总表和蓝色核对区。", "任意差异非 0 时确认。"],
  279. ["报告生成", "生成客户可核对的结算报告。", "Excel/Word/PDF 结算报告。", "发送客户前确认。"],
  280. ["状态记录", "记录待客户核对、待盖章、待开票、待付款、已完成。", "结算状态和提醒。", "状态变更确认。"],
  281. ],
  282. [1450, 3350, 2300, 2270],
  283. )
  284. add_heading(doc, "7. 落地路线", 1)
  285. add_table(
  286. doc,
  287. ["阶段", "目标", "交付内容"],
  288. [
  289. ["阶段 1:Agent 底座稳定", "让业务查询不再依赖固定任务匹配。", "多步 Agent loop、smart query、schema 工具、通用表格导出。"],
  290. ["阶段 2:结算场景跑通", "先完成铜仁移动等月结企业的报告生成闭环。", "导出接口调用、Excel 合并、汇总计算、差异标记、人工确认清单。"],
  291. ["阶段 3:充值/消费扩展", "把充值导入、消费履约、配送状态纳入 Agent。", "充值校验、分仓订单、供应商/物流状态查询。"],
  292. ["阶段 4:治理与上线", "支持多人使用,同时可审计、可限流、可回归测试。", "中心 MCP、连接池、权限、trace 面板、评测集。"],
  293. ],
  294. [1500, 3300, 4560],
  295. )
  296. add_heading(doc, "8. 后续需要确认的信息", 1)
  297. add_bullet(doc, "结算表最终标准模板:客户侧希望看到的列、盖章版格式、开票字段。")
  298. add_bullet(doc, "各导出接口真实参数:月份、企业渠道、订单状态、是否已付款、是否排除逻辑删除。")
  299. add_bullet(doc, "运费账单和订单表的稳定关联字段:订单号、第三方订单号、运单号是否一一对应。")
  300. add_bullet(doc, "商品编码补齐口径:海博编码、商品编码、SKU 编码分别来自哪些表和字段。")
  301. add_bullet(doc, "人工确认流程入口:是在 Agent 对话里确认,还是推到业务系统待办/审批。")
  302. add_callout(
  303. doc,
  304. "建议",
  305. "下一步优先把“铜仁移动 2026 年 5 月结算”作为第一条端到端样例做穿:导出原始表、补齐商品字段、计算汇总、标记差异、生成 Excel 报告。等这条链路稳定,再复制到招商银行贵阳分行和中数未来。"
  306. )
  307. OUT.parent.mkdir(parents=True, exist_ok=True)
  308. doc.save(OUT)
  309. print(OUT)
  310. if __name__ == "__main__":
  311. build_doc()