微信扫码
添加专属顾问
我要投稿
AI助力标书制作,告别繁琐格式调整,让投标人重获自由! 核心内容: 1. 标书制作的痛点:格式调整与图片插入的繁琐流程 2. AI解决方案:智能解析招标文件,自动提取并精准插入附件 3. 实践案例:知识库管理与企业资质图片的自动化处理
"投标人员的时间守恒定律:格式调整耗时 = 插入图片数 × 标书页数/10" ⏰
graph LR
A[招标文件] --> B[智能解析]
B --> C[自动提取附件]
C --> D[精准插入图片]
D --> E[完美格式标书]
温馨提示:观看时请勿羡慕到流泪 😭
数据是LLM的血液,没有数据的Agent就像没有汽油的超跑 —— 只能看不能开
我们采用"分治策略"管理企业资质:
知识库:这里存放着价值百万的"数字房产证"🏠
温馨提示:本流程已通过ISO-9001"防加班"认证 🕒
import requests
import tempfile
import os
import re
from collections.abc import Generator
from typing import Any, Dict, List, Optional
from io import BytesIO
from docx import Document
from dify_plugin import Tool
from dify_plugin.entities.tool import ToolInvokeMessage
class DocxTitleTool(Tool):
def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage]:
"""
提取DOCX文件中的标题
Args:
tool_parameters: 包含docx文件和title_level参数
"""
# 获取参数
uploaded_file = tool_parameters.get('docx')
title_level = tool_parameters.get('title_level', 1)
if not uploaded_file:
yield self.create_text_message("请上传DOCX文件")
return
try:
title_level = int(title_level)
if title_level < 1 or title_level > 9:
yield self.create_text_message("标题级别必须在1-9之间")
return
except (ValueError, TypeError):
yield self.create_text_message("标题级别必须是有效的数字")
return
try:
# 下载文件
file_url = "http://api:5001" + uploaded_file.url
response = requests.get(file_url)
if response.status_code != 200:
yield self.create_text_message(f"文件下载失败,状态码: {response.status_code}")
return
# 创建临时文件保存DOCX内容
with tempfile.NamedTemporaryFile(suffix='.docx', delete=False) as temp_file:
temp_file.write(response.content)
temp_file_path = temp_file.name
try:
# 提取标题
headings_result = self._extract_headings_by_level(temp_file_path, title_level)
if not headings_result:
yield self.create_text_message("未在文档中找到指定级别的标题")
return
# 返回结果
yield self.create_json_message({
"result": headings_result,
"summary": f"成功提取了{title_level}级及以上的标题"
})
finally:
# 清理临时文件
if os.path.exists(temp_file_path):
os.unlink(temp_file_path)
except Exception as e:
yield self.create_text_message(f"处理文件时发生错误: {str(e)}")
def _get_heading_level(self, paragraph) -> Optional[int]:
"""
获取段落的标题级别
Args:
paragraph: 文档段落对象
Returns:
int: 标题级别 (1-9),如果不是标题则返回None
"""
# 方法1: 通过样式名称判断
style_name = paragraph.style.name.lower()
# 检查标准的标题样式
if 'heading' in style_name:
# 提取数字,如 'Heading 1' -> 1
match = re.search(r'heading\s*(\d+)', style_name)
if match:
return int(match.group(1))
# 检查中文标题样式
if '标题' in style_name:
match = re.search(r'标题\s*(\d+)', style_name)
if match:
return int(match.group(1))
# 方法2: 通过outline_level属性判断
if hasattr(paragraph, '_p') and hasattr(paragraph._p, 'pPr'):
pPr = paragraph._p.pPr
if pPr is not None and hasattr(pPr, 'outlineLvl_val'):
outline_level = pPr.outlineLvl_val
if outline_level is not None:
return outline_level + 1 # outline_level是0-based,转换为1-based
# 方法3: 通过样式的outline_level属性
try:
if hasattr(paragraph.style, '_element') and hasattr(paragraph.style._element, 'pPr'):
pPr = paragraph.style._element.pPr
if pPr is not None:
outline_lvl = pPr.find('.//{http://schemas.openxmlformats.org/wordprocessingml/2006/main}outlineLvl')
if outline_lvl is not None:
val = outline_lvl.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val')
if val is not None:
return int(val) + 1
except:
pass
return None
def _extract_headings_by_level(self, file_path: str, max_level: int) -> List[Dict[str, List[str]]]:
"""
提取指定级别的标题
Args:
file_path: DOCX文件路径
max_level: 最大标题级别
Returns:
List[Dict[str, List[str]]]: 按用户要求格式返回的标题列表
"""
try:
# 加载文档
doc = Document(file_path)
# 按级别收集标题
headings_by_level = {}
for level in range(1, max_level + 1):
headings_by_level[level] = []
# 遍历所有段落
for paragraph in doc.paragraphs:
# 跳过空段落
if not paragraph.text.strip():
continue
# 获取标题级别
heading_level = self._get_heading_level(paragraph)
if heading_level is not None and heading_level <= max_level:
headings_by_level[heading_level].append(paragraph.text.strip())
# 转换为用户要求的格式
result = []
level_names = {
1: "一级标题",
2: "二级标题",
3: "三级标题",
4: "四级标题",
5: "五级标题",
6: "六级标题",
7: "七级标题",
8: "八级标题",
9: "九级标题"
}
for level in range(1, max_level + 1):
if headings_by_level[level]: # 只添加有内容的级别
level_name = level_names.get(level, f"{level}级标题")
result.append({level_name: headings_by_level[level]})
return result
except Exception as e:
raise Exception(f"提取标题时发生错误: {str(e)}")
你是一个招标文件分析助手,在招投标领域,乙方投标文件,必须按照甲方招标文件中规定的格式进行撰写
#输入是招标文件的章节标题,请判断对于投标文件的格式要求,所在的章节,然后返回开始章节标题,和结束章节标题
#符合条件的章节,一般包含有"附件"字样,即该章,附带了许多格式化的表格,投标人照此写标书
#如果没有结束章节标题,则置为空
参考输出格式:
{"开始标题":”XXX“,
”结束标题“:"XXX"}
{{#投标文件的章节名称.result#}}
/no_think
经过LLM后,去掉输出结果的think标签
import re,json
from typing import List
import json
def main(arg1):
tmp = re.sub(r"<think>[\s\S]*?</think>", "", arg1, flags=re.DOTALL)
tmp = re.sub(r'^\s*```(?:json)?\s*\n?', '', tmp,flags=re.IGNORECASE)
tmp = re.sub(r'\n?```\s*$', '', tmp)
tmp = tmp.replace(r'\n', '\n').replace(r'\"', '"').strip()
result=tmp
return {
'result':result
}
# ===== 数据结构定义 =====
定义 ElementInfo:
元素类型: 'paragraph' 或 'table'
XML内容: 字符串
文本内容: 字符串 (用于预览)
索引: 整数
分区格式: 字典 (可选)
定义 SectionInfo:
标题: 字符串
元素列表: [ElementInfo]
定义 PageFormatInfo:
页面宽度, 高度, 方向, 四周边距: 整数/字符串
定义 DocumentExtractResult:
章节列表: [SectionInfo]
页面格式: PageFormatInfo
源文件路径: 字符串
元素总数: 整数
抽取信息: 字典
分区格式列表: [字典] (可选)
样式XML: 字符串 (可选)
编号XML: 字符串 (可选)
# ===== 文档解析器类 =====
类 DocxExtractor:
初始化(文档路径):
加载DOCX文档
获取页面格式(章节索引=0):
尝试从文档获取页面设置
失败则返回默认A4格式
查找单章节(开始标题, 标题级别=1, 结束标题=None):
预处理标题(移除空格)
收集所有文档元素(段落+表格)
遍历元素:
- 发现匹配的开始标题 → 创建新章节
- 继续收集元素直到:
a) 遇到结束标题
b) 遇到同级标题(无结束标题时)
返回找到的章节列表
抽取单章节(开始标题, 标题级别=1, 结束标题=None):
调用查找单章节
收集分区格式信息
确定最终页面格式:
优先使用提取的分区格式
否则调用获取页面格式
构建DocumentExtractResult对象
返回结果
# ===== 插件工具类 =====
类 DocxExtractorTool (继承Tool):
执行入口(参数):
验证必要参数(文档文件, 开始标题)
下载DOCX文件 → 保存为临时文件
创建DocxExtractor实例
调用抽取单章节方法
处理结果:
添加成功元数据
转换为JSON格式
生成输出文件
异常处理:
值错误/通用错误 → 生成错误JSON文件
返回处理结果(文件blob或错误信息)
// XML数据结构:标书的DNA
{
"element_type": "paragraph",
"xml_content": "<w:p>7-2纳税证明</w:p>",
"text_content": "7-2纳税证明",
"index": 642
}
智能匹配流程:
a.文字:"7-2纳税证明"发出相亲请求
b.知识库:匹配到纳税证明图片集
c.LLM媒人:生成"结婚证"(XML数据)
d.输出:文字+图片的完美组合
大模型将图片转换为xml的prompt:
你是一个json数据处理专家,请将数据A,按照参考格式,进行构造,然后返回一个新的json数据
#如果待处理数据只有文字,则生成element_type为paragraph。
如果待处理数据还有图像,则生成element_type为picture。
#index字段,与B保持一致,举例B为100,则输出的所有index都是100
数据A:
{{检索到的图片url.result#}}
数据B:
{{#当前数据索引#}}
如果只有文字,输出格式参考
{
"element_type": "paragraph",
"xml_content": "xxxx",
"text_content": "xxx",
"index": xxx,
"section_format": null
}
如果是图像,输出格式参考
{
"element_type": "picture",
"image_path": "XXX,
"index": XXX
}
函数 assemble_document(输入: 抽取结果, 输出路径, 是否清理分区属性):
尝试:
创建临时目录和DOCX基础结构
初始化XML内容列表
添加DOCX文档头部到XML列表
收集所有元素并按原始索引排序
遍历每个元素:
如果元素类型是图片:
从Docker获取图片数据
生成图片XML并保存到媒体目录
将图片XML添加到内容列表
如果元素类型是段落或表格:
获取元素XML内容
根据标志决定是否清理分区属性
将处理后的XML添加到内容列表
添加DOCX文档尾部到XML列表
将完整XML写入document.xml文件
创建辅助文件:
[Content_Types].xml
主关系文件(.rels)
文档关系文件(document.xml.rels)
样式文件(styles.xml)
编号文件(numbering.xml)
将临时目录打包为DOCX文件
输出处理统计信息
异常处理:
抛出错误信息
最终:
清理临时目录
使用xml标签,确保了生成标书的格式的正确性,而且自动插入图片,节省了投标人员一部分时间。
通过XML标签实现:
最终效果:投标人员终于可以笑着交标书,而不是熬夜改格式 😄
如果这篇技术分享对您的AI落地实践有所启发,欢迎点赞、转发!
有任何技术问题或合作意向,欢迎在评论区留言交流 ~~~
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2025-09-15
淘宝悄悄上线了AI导购,懒人购物原来可以这么爽。
2025-09-12
成本砍掉 90%!用n8n、多维表格和Nano Banana,搭全自动AI试衣工作流,上新速度提升 1000%!
2025-09-07
零售AI:90%在用,20%营收差定生死
2025-09-07
从增量优化到指数级变革:一份关于B2B营销AI实践的深度诊断与转型路线图
2025-08-29
{标签科学}AI Open了新场景,新增量
2025-08-29
AI赋能品牌节点营销:碎片化时代提升创意与效率的策略
2025-08-28
GEO(生成式引擎优化)在20大重点行业的应用与解决方案
2025-08-21
如何用 AI 做营销:问题不是如何提效,而是底层打法变了
2025-06-24
2025-08-28
2025-06-26
2025-08-05
2025-08-13
2025-08-10
2025-09-12
2025-07-18
2025-08-29
2025-08-21
2025-06-26
2025-06-15
2025-06-03
2025-05-29
2025-05-26
2025-05-22
2025-05-21
2025-05-18