You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

461 lines
27 KiB

import uuid
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from qwen_agent.agents import Assistant
import json_repair
import json
embeddings = DashScopeEmbeddings(dashscope_api_key="sk-ea89cf04431645b185990b8af8c9bb13")
device_id=0
import re
import time
from docx import Document
import shutil
from docx.opc.pkgreader import _SerializedRelationships, _SerializedRelationship
from docx.opc.oxml import parse_xml
import logging
import logging.config
import requests
from collections import defaultdict
userLog=None
def load_from_xml_v2(baseURI, rels_item_xml):
"""
Return |_SerializedRelationships| instance loaded with the
relationships contained in *rels_item_xml*. Returns an empty
collection if *rels_item_xml* is |None|.
"""
srels = _SerializedRelationships()
if rels_item_xml is not None:
rels_elm = parse_xml(rels_item_xml)
for rel_elm in rels_elm.Relationship_lst:
if rel_elm.target_ref in ('../NULL', 'NULL'):
continue
srels._srels.append(_SerializedRelationship(baseURI, rel_elm))
return srels
_SerializedRelationships.load_from_xml = load_from_xml_v2
# 记录程序开始的时间戳
def getOutlineLevel(inputXml):
"""
功能 从xml字段中提取出<w:outlineLvl w:val="number"/>中的数字number
参数 inputXml
返回 number
"""
start_index = inputXml.find('<w:outlineLvl')
end_index = inputXml.find('>', start_index)
number = inputXml[start_index:end_index + 1]
number = re.search("\d+", number).group()
return number
def isTitle(paragraph):
"""
功能 判断该段落是否设置了大纲等级
参数 paragraph:段落
返回 None:普通正文,没有大纲级别 0:一级标题 1:二级标题 2:三级标题
"""
# 如果是空行,直接返回None
if paragraph.text.strip() == '':
return None
# 如果该段落是直接在段落里设置大纲级别的,根据xml判断大纲级别
paragraphXml = paragraph._p.xml
if paragraphXml.find('<w:outlineLvl') >= 0:
return getOutlineLevel(paragraphXml)
# 如果该段落是通过样式设置大纲级别的,逐级检索样式及其父样式,判断大纲级别
targetStyle = paragraph.style
while targetStyle is not None:
# 如果在该级style中找到了大纲级别,返回
if targetStyle.element.xml.find('<w:outlineLvl') >= 0:
return getOutlineLevel(targetStyle.element.xml)
else:
targetStyle = targetStyle.base_style
# 如果在段落、样式里都没有找到大纲级别,返回None
return None
#寻找标题名称
def findTitleName(docxPath):
yield '文档相似性检查----检查是否存在详细设计方案'
loopCount = 0
while True:
loopCount+=1
if(loopCount>=15):
raise Exception("文档读取超时,或文档存在问题无法读取")
break
try:
document = Document(docxPath)
break
except Exception as e:
time.sleep(1)
pass
# 逐段读取docx文档的内容
titleWords=[]
firstTitle = 0
secondTitle = 0
sanjiTitle = 0
for paragraph in document.paragraphs:
# 判断该段落的标题级别
# 这里用isTitle()临时代表,具体见下文介绍的方法
text = paragraph.text
if text.strip():#非空判断
level = isTitle(paragraph)
if level=="0":
firstTitle+=1
secondTitle = 0
if(text.find("附件")>=0):
continue
titleWords.append("一级标题:".format(firstTitle)+text)
elif level=="1":
secondTitle+=1
sanjiTitle=0
# words.append("\t"+"{}.{}".format(firstTitle,secondTitle)+text)
# titleWords.append("第{}章的二级标题:".format(firstTitle,firstTitle,secondTitle)+text)
elif level=="2":
sanjiTitle += 1
# words.append("\t"+"{}.{}".format(firstTitle,secondTitle)+text)
# titleWords.append("第{}章的三级标题".format(firstTitle, secondTitle,firstTitle, secondTitle,sanjiTitle) + text)
findTitleName_llm_cfg = {
#'model': 'qwen1.5-72b-chat',
'model':"qwen2-72b",
'model_server': 'http://127.0.0.1:1025/v1', # base_url, also known as api_base
# 'api_key': 'sk-ea89cf04431645b185990b8af8c9bb13',
}
findTitleName_bot = Assistant(llm=findTitleName_llm_cfg,
name='Assistant',
# system_message='1:这样的是一级标题。1.1:这样的是二级标题。1.1.1:这样的是三级标题'
)
prompt='''\n是文档的大纲,一级标题组成,哪一章存在与方案相关的内容
类似详细设计方案,详细服务方案,详细建设方案为最相关的,优先选择
类似设计方案,服务方案,建设方案为次相关,次级选择
类似方案是最后选择
按照这样的顺序选择最合适的
你只能从这两个答案中选择一个:{"name":"一级标题名称","answer":"存在"}或{"name":"","answer":"不存在"},不做过多的解释,严格按回答格式作答
'''
# print("\n".join(titleWords)+prompt)
messages = [({'role': 'user', 'content': "\n".join(titleWords)+prompt})]
runList=[]
for rsp in findTitleName_bot.run(messages):
runList.append(rsp)
data = runList[len(runList) - 1][0]["content"]
parsed_data = json_repair.loads(data.replace('`', ''))
if(parsed_data["answer"]=="存在"):
yield parsed_data["name"]
else:
yield "文档相似性检查----未找到与详细设计方案相关内容,无法进行相似性比较"
def merge_chapters(words):
merged_text = {}
for line in words:
if "" in line:
key, value = line.split("", 1) # 根据第一个冒号分割
if key in merged_text:
merged_text[key].append(value.strip()) # 添加到列表
else:
merged_text[key] = [value.strip()] # 初始化列表
else:
logging.warning(f"Skipping line without key-value pair: {line}")
# 合并结果格式化为列表输出
merged_words = []
for key, values in merged_text.items():
combined_value = "".join(values) # 将内容合并
merged_words.append(f"{key}{combined_value}")
return merged_words
#获取文档中 详细设计方案 章节的所有内容
def getDocxToText(docxPath, titleName, vector_store_path):
loopCount = 0
while True:
loopCount += 1
if loopCount >= 15:
raise Exception("文档读取超时,或文档存在问题无法读取")
break
try:
document = Document(docxPath)
break
except Exception as e:
time.sleep(1)
pass
# 逐段读取docx文档的内容
levelList = []
words = []
addStart = False
title_counter = [] # 用于存储当前标题的计数
title_texts = [] # 用于存储当前各级标题的文本
i = 0
for paragraph in document.paragraphs:
text = paragraph.text.strip()
if text: # 非空判断
level = isTitle(paragraph) # 确保这个函数在代码中定义
# 当前标题的层级
current_level = int(level) if level is not None else -1
if current_level >= 0: # 标题段落
# 确保标题计数器足够长
while len(title_counter) <= current_level:
title_counter.append(0) # 初始化新级别的标题计数
title_texts.append('') # 初始化对应的标题文本
# 更新当前级别及以下的标题计数和标题文本
title_counter[current_level] += 1 # 当前级别计数加1
title_counter = title_counter[:current_level+1]
title_texts[current_level] = text # 保存当前级别的标题文本
title_texts = title_texts[:current_level+1]
# 重置更低级别的计数和标题文本
for idx in range(current_level + 1, len(title_counter)):
title_counter[idx] = 0
title_texts[idx] = ''
# 检查是否与 titleName 匹配
if current_level == 0:
addStart = titleName in text # 检查是否与 titleName 匹配
else: # 非标题段落
if addStart:
if len(text) > 30: # 仅记录长度大于30的内容
i += 1
# 获取当前完整的标题编号和标题名称
levelText = ".".join(map(str, title_counter))
# 使用非空的标题名称
current_title = title_texts[-1] if title_texts else ''
words.append(f"{levelText}-{current_title}{text}")
if len(words) == 0:
raise Exception("checkRepeatText,获取长度为0")
# 使用封装的合并函数
merged_words = merge_chapters(words)
# 将合并后的内容写入 txt 文件
with open("checkRepeatText.txt", 'w') as txt_file:
for line in merged_words:
txt_file.write(f"{line}\n")
time.sleep(3)
# 加载文本
loader = TextLoader(file_path='checkRepeatText.txt')
docs = loader.load()
# 创建唯一标识符
uuids = []
for _ in range(len(merged_words)):
uuids.append(str(uuid.uuid4()))
logging.info(f"checkRepeatTextuuidLen{len(uuids)}")
return merged_words, uuids
# @app.route('/checkRepeatText/<filename>', methods=['GET'])
def checkRepeatText(filename):
yield "文档相似性检查---启动中...."
vector_store_path="vector_store"+str(uuid.uuid4())
for titleName in findTitleName(filename):
yield titleName
if(titleName!="文档相似性检查----未找到与详细设计方案相关内容,无法进行相似性比较"):
yield "文档相似性检查----文档内容解析中"
words,uuids=getDocxToText(filename,titleName,vector_store_path)
# 记录程序开始的时间戳‘
reslist = []
count = 0
standard = {
"清晰性": """对软件功能描述的完整性主要体现在以下两个方面:
a. 功能描述是否简洁明了,避免使用过于复杂或专业的术语,使得用户能够轻松理解。
b. 是否明确指出了功能的具体作用,没有模糊不清或含糊其辞的表述。
如果要将软件功能描述的清晰性划分为优秀、良好、一般、差四个从高到低的等级,每个等级的评判标准是什么?
将软件功能描述的清晰性划分为优秀、良好、一般、差四个等级时,每个等级的评判标准可以如下定义:
优秀(90~100分)
简洁明了:功能描述极其精炼,没有多余的词汇,每个字都承载着必要的信息。
通俗易懂:完全避免了专业术语或行业黑话,即使是非专业用户也能轻松理解。
具体明确:功能的作用、范围、限制以及用户期望的结果都被清晰、准确地阐述,没有任何模糊或含糊的表述。
良好(70分~90分,不包含90分)
较为简洁:功能描述相对简短,但可能包含一些必要的细节或背景信息。
易于理解:大部分术语都是通俗易懂的,对于少数专业术语,提供了简短的解释或上下文。
明确具体:功能的主要作用、范围和用户期望的结果都被明确阐述,但可能在某些细节上稍显模糊。
一般(60~70分,不包含70分)
稍显冗长:功能描述可能包含一些不必要的细节或重复信息,导致用户需要花费更多时间来理解。
有一定难度:使用了一些专业术语或行业黑话,但没有提供足够的解释或上下文,导致非专业用户可能难以理解。
基本明确:功能的主要作用被阐述,但在范围、限制或用户期望的结果上可能存在一些模糊或含糊的表述。
差(60分以下,不包含60分)
冗长复杂:功能描述过于详细和复杂,包含大量不必要的细节和背景信息,导致用户难以抓住重点。
难以理解:大量使用专业术语或行业黑话,且没有提供任何解释或上下文,使得大部分用户都难以理解。
模糊不清:功能的作用、范围、限制以及用户期望的结果都没有被明确阐述,存在大量的模糊和含糊表述。
评估的提示词举例:
根据这些评判标准,对下面的软件功能描述的清晰性进行客观的评价,给出优秀、良好、一般、差四个等级之一的评价,并给出具体得分。并在此基础上润色和完善,使之达到优秀的等级。
""",
"完整性": """对软件功能描述的完整性主要体现在以下两个方面:
a. 是否涵盖了功能的所有重要方面,包括输入、输出、处理过程等。
b. 是否提供了足够的信息,以便用户能够全面了解功能的工作原理和用途。
如果要将软件功能描述的完整性划分为优秀、良好、一般、差四个从高到低的等级,每个等级的评判标准是什么?
将软件功能描述的完整性划分为优秀、良好、一般、差四个等级时,每个等级的评判标准可以如下定义:
优秀:(90~100分)
描述全面涵盖了功能的所有重要方面,包括但不限于输入、输出、处理过程、异常处理等。
提供了详尽的信息,用户能够清晰地了解功能的工作原理、用途以及在不同场景下的表现。
包含了必要的示例、图表或流程图,以直观展示功能的工作流程和效果。
没有遗漏任何对用户理解和使用功能至关重要的信息。
良好:(70分~90分,不包含90分)
描述基本涵盖了功能的主要方面,但可能有个别不太重要的细节未提及。
提供了足够的信息,用户能够较好地理解功能的工作原理和用途,但在某些复杂场景下可能需要额外说明。
可能包含一些示例或图表,但可能不如优秀等级那么全面或详细。
一般:(60~70分,不包含70分)
描述涵盖了功能的一部分重要方面,但存在较明显的遗漏或不足。
提供的信息有限,用户可能只能对功能有一个大致的了解,无法深入了解其工作原理和详细用途。
可能缺乏示例、图表或流程图等辅助材料,导致用户难以理解功能的某些复杂部分。
差:(60分以下,不包含60分)
描述严重缺失,未涵盖功能的关键方面,甚至可能误导用户。
提供的信息极少,用户无法全面了解功能的工作原理和用途。
可能存在错误或矛盾的信息,导致用户无法准确理解功能。
根据这些评判标准,对下面的软件功能描述的完整性进行客观的评价,给出优秀、良好、一般、差四个等级之一的评价。并在此基础上润色和完善,使之达到优秀的等级。
""",
"可测试性": """软件功能描述的可测试性主要体现为以下方面:
a. 功能描述是否具体、明确,以便能够进行功能测试和验证。
b. 是否提供了足够的细节,以便开发人员和测试人员能够准确理解和实现功能。
如果要将软件功能描述的可测试性划分为优秀、良好、一般、差四个从高到低的等级,每个等级的评判标准是什么?
将软件功能描述的可测试性划分为优秀、良好、一般、差四个等级时,每个等级的评判标准可以如下定义:
优秀:(90~100分)
功能描述非常具体和明确,能够直接转化为测试用例。
提供了详尽的细节,包括输入、输出、边界条件、异常处理等。
开发人员和测试人员能够轻松理解和实现功能,无需额外澄清或假设。
功能描述中包含了预期的行为和非预期的行为,有助于全面覆盖测试场景。
良好:(70分~90分,不包含90分)
功能描述相对具体和明确,大部分内容可以直接用于测试。
提供了足够的细节,但可能需要一些额外的解释或澄清才能完全理解。
开发人员和测试人员能够基于描述实现和测试功能,但可能需要一些额外的沟通和协调。
功能描述中基本涵盖了主要的行为和边界条件,但可能缺少对某些异常情况的详细描述。
一般:(60~70分,不包含70分)
功能描述较为笼统,需要较多的解释和澄清才能用于测试和开发。
细节不够充分,可能导致开发人员和测试人员在实现和测试过程中产生误解或遗漏。
需要较多的沟通和协调来确保功能的正确实现和测试。
功能描述中可能只涵盖了主要的行为,对边界条件和异常情况的描述较为模糊或缺失。
差:(60分以下,不包含60分)
功能描述非常模糊和笼统,无法直接用于测试和开发。
缺乏必要的细节,导致开发人员和测试人员无法准确理解和实现功能。
需要大量的沟通和协调,甚至可能需要重新编写功能描述才能进行有效的测试和开发。
功能描述中可能只提到了大致的目标或意图,没有具体的行为描述、边界条件或异常处理。
根据这些评判标准,对下面的软件功能描述的可测试性进行客观的评价,给出优秀、良好、一般、差四个等级之一的评价。并在此基础上润色和完善,使之达到优秀的等级。
""",
"详细性": """软件功能详细性主要体现在:
a. 功能描述是否详细,可以根据功能描述进行功能点评价,计算出ILF、EIF、EI、EO、EQ的数量;
如果要将软件功能描述的详细性划分为优秀、良好、一般、差四个从高到低的等级,每个等级的评判标准是什么?
将软件功能描述的详细性划分为优秀、良好、一般、差四个等级时,每个等级的评判标准可以如下定义:
优秀:(90~100分)
功能描述非常详尽,包含了所有必要的信息,使得评估者能够轻松地根据描述进行功能点评价。
ILF、EIF、EI、EO、EQ的数量可以明确且无误地计算出来,没有遗漏或模糊之处。
描述中不仅包含了功能的正常操作,还涵盖了异常处理、边界条件等特殊情况。
使用了具体的例子、流程图或伪代码来进一步阐明功能。
良好:(70分~90分,不包含90分)
功能描述相对详细,提供了足够的信息来进行功能点评价。
ILF、EIF、EI、EO、EQ的数量可以大致计算出来,但可能需要一些额外的解释或澄清。
描述中基本涵盖了功能的各个方面,但对某些细节或特殊情况可能描述不够充分。
整体而言,描述是清晰和准确的,但还有改进的空间。
一般:(60~70分,不包含70分)
功能描述较为笼统,缺乏具体的细节。
ILF、EIF、EI、EO、EQ的数量计算可能存在一定的困难或不确定性,需要较多的假设或推测。
描述中只涵盖了功能的主要方面,对细节和特殊情况的处理描述不足。
可能需要额外的沟通或澄清才能准确理解功能需求。
差:(60分以下,不包含60分)
功能描述非常模糊,缺乏必要的信息和细节。
无法根据描述进行准确的功能点评价,ILF、EIF、EI、EO、EQ的数量无法确定。
描述中可能只提到了功能的大致目标或意图,没有具体的实现细节或操作步骤。
需要大量的额外信息或澄清才能理解功能需求,甚至可能需要重新编写功能描述。
根据这些评判标准,对下面的软件功能描述的详细性进行客观的评价,给出优秀、良好、一般、差四个等级之一的评价。并在此基础上润色和完善,使之达到优秀的等级。
""",
}
weight = {
"清晰性" : 0.4,
"完整性" : 0.3,
"可测试性" : 0.2,
"详细性" : 0.1,
}
findTitleName_llm_cfg = {
'model': "qwen2-72b",
'model_server': 'http://127.0.0.1:1025/v1',
}
findTitleName_bot = Assistant(llm=findTitleName_llm_cfg, name='Assistant')
for i in words:
count += 1
yield f"文档相似性检查--对{titleName}章节,进行文档内容检查中{count}/{len(words)}"
chapter, rest = i.split('-', 1)
title, text = rest.split('', 1)
# 生成字典
example = {
"chapter": chapter.strip(),
"title": title.strip(),
"text": text.strip()
}
result = {
"title": title.strip(),
"text": text.strip()
}
# 循环提取键和值
weighted_score = 0
for key, value in standard.items():
prompt_score = f"""对软件功能{key}的定义:
{value}
模块名称:【{example['title']}
模块描述:【{example['text']}
回答格式为:{{"模块名称""{example['text']}",
"等级":"优秀/良好/一般/差",
"得分":"0~100",
"理由及扣分原因":"理由及扣分原因",
}},不做过多的解释,严格按回答格式作答,只给出一个回答。
"""
messages = [({'role': 'user', 'content': prompt_score})]
runList = []
for rsp in findTitleName_bot.run(messages):
runList.append(rsp)
data = runList[len(runList) - 1][0]["content"]
parsed_data = json_repair.loads(data.replace('`', ''))
if isinstance(parsed_data, list): # 检查parsed_data是否为列表
parsed_data = parsed_data[0] # 取第一个元素
else:
parsed_data = parsed_data
result[f"{key}等级"] = parsed_data['等级']
result[f"{key}得分"] = parsed_data['得分']
score = int(parsed_data['得分']) # 假设 '得分' 是字符串,需要转换为整数
key_weight = weight.get(key, 0) # 根据键获取权重,如果没有匹配的权重,默认为 0
# 计算加权得分并累加
weighted_score += score * key_weight
result["加权得分"] = round(weighted_score, 2) # 保留两位小数
answer = f"{example['text']}"
for key, value in standard.items():
prompt_answer = f"""对软件功能{key}的定义:\n
{value}\n
模块名称:【{example['title']}\n
模块描述:f【{answer}\n
回答格式为:{{"模块名称""{example['text']}",
"改进后的描述":"改进后的描述",
}},不做过多的解释,严格按回答格式作答。
"""
messages = [({'role': 'user', 'content': prompt_answer})]
runList = []
for rsp in findTitleName_bot.run(messages):
runList.append(rsp)
data = runList[len(runList) - 1][0]["content"]
parsed_data = json_repair.loads(data.replace('`', ''))
answer = parsed_data['改进后的描述']
result["改进后的描述"] = answer
textTag = i.split("")[0]
breakpoint()
# vectorstore.delete(ids=uuids)
shutil.rmtree(vector_store_path)
resInfo=f"{titleName}章节,发现相似内容:<br>"
if(len(reslist)>0):
for res in reslist:
resInfo+="【在**"+res["yuanwen1"][:res["yuanwen1"].find('')]+"**下包含:"+res["yuanwen1"][res["yuanwen1"].find('') + 1:]+"<br>在**"+res["yuanwen2"][:res["yuanwen2"].find('')]+"**下包含:"+res["yuanwen2"][res["yuanwen2"].find('') + 1:]+"<br>以上两段内容***相似度***:"+'{:.2f}'.format(res['similarity'])+"】<br>"
yield resInfo
else:
yield "**未发现相似内容**"
userLog.info("文档相似性检查----未发现相似内容**")
for i in checkRepeatText("./北仑区综合行政执法局协同监管系统项目建设方案_20240824.docx"):
print(i)