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字段中提取出中的数字number 参数 inputXml 返回 number """ start_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('= 0: return getOutlineLevel(paragraphXml) # 如果该段落是通过样式设置大纲级别的,逐级检索样式及其父样式,判断大纲级别 targetStyle = paragraph.style while targetStyle is not None: # 如果在该级style中找到了大纲级别,返回 if targetStyle.element.xml.find('= 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/', 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}章节,发现相似内容:
" if(len(reslist)>0): for res in reslist: resInfo+="【在**"+res["yuanwen1"][:res["yuanwen1"].find(':')]+"**下包含:"+res["yuanwen1"][res["yuanwen1"].find(':') + 1:]+"
在**"+res["yuanwen2"][:res["yuanwen2"].find(':')]+"**下包含:"+res["yuanwen2"][res["yuanwen2"].find(':') + 1:]+"
以上两段内容***相似度***:"+'{:.2f}'.format(res['similarity'])+"】
" yield resInfo else: yield "**未发现相似内容**" userLog.info("文档相似性检查----未发现相似内容**") for i in checkRepeatText("./北仑区综合行政执法局协同监管系统项目建设方案_20240824.docx"): print(i)