import json import os import re import time from collections import defaultdict from flask_app.general.doubao import read_txt_to_string from flask_app.general.file2markdown import convert_file_to_markdown from flask_app.general.format_change import get_pdf_page_count, pdf2docx from flask_app.general.json_utils import extract_content_from_json from flask_app.general.model_continue_query import process_continue_answers from flask_app.general.通义千问long import upload_file, qianwen_long, qianwen_plus def remove_unknown_scores(data): if isinstance(data, dict): return { k: remove_unknown_scores(v) for k, v in data.items() if not (k == "评分" and v in ["未知", "/", ""]) } elif isinstance(data, list): return [remove_unknown_scores(item) for item in data] else: return data def combine_technical_and_business(data): data = remove_unknown_scores(data) extracted_data = { '技术评分': { '技术评分': {} # 初始化技术评分 }, '商务评分': { '商务评分': {}, # 初始化商务评分 '投标报价评分': {} # 初始化投标报价评分 # '其他评分' 将在需要时动态添加 } } def extract_nested(data): if isinstance(data, dict): for key, value in data.items(): # 区分 '技术评分' if key == '技术评分': total_score=compute_total_score({key:value}) extracted_data['技术评分']['技术评分'] = value # 匹配到后,不再递归处理其子项 continue # 区分 '商务评分' elif key == '商务评分': extracted_data['商务评分']['商务评分'] = value # 匹配到后,不再递归处理其子项 continue # 区分 '投标报价评分' elif key == '投标报价评分': extracted_data['商务评分']['投标报价评分'] = value # 匹配到后,不再递归处理其子项 continue # 其他键名归为 '其他评分' else: if '其他评分' not in extracted_data['商务评分']: extracted_data['商务评分']['其他评分'] = {} extracted_data['商务评分']['其他评分'][key] = value continue extract_nested(data) return extracted_data # 防止外键只有一个'一包'的情况 def process_data_based_on_key(data): exclude_word = ["包", "未知", "评分因素"] # 获取字典的键列表 keys = list(data.keys()) # 检查键的数量是否为1并且 exclude_word 中的任何词包含在 keys[0] 中 if len(keys) == 1 and any(word in keys[0] for word in exclude_word): # 返回内层的字典 return data[keys[0]] # 如果条件不满足,则返回原始字典 return data def compute_total_score(data): """ 计算传入字典的总分。 规则: - 输入字典只有一个外层键。 - 遍历该外层键的所有子键: - 如果子键名中包含 '(XX分)' 或 '(XX分)',提取 XX 并累加到总分中,不再处理其子项。 - 如果子键名中不包含这样的分数,遍历其子项,查找键名为 '评分' 的键,提取分数并累加。 - '评分' 的值可以是 'XX分' 或整数。 - 如果没有找到 '评分' 键,则该项分数为 0。 """ total = 0 # 确保输入数据为字典且只有一个外层键 if not isinstance(data, dict) or len(data) != 1: raise ValueError("输入数据必须是一个只有一个外层键的字典。") # 获取唯一的外层键和值 outer_key, outer_value = next(iter(data.items())) # 更新后的正则表达式,匹配中英文括号中的分数,如 '(24分)' 或 '(24分)' score_pattern = re.compile(r'[((](\d+)分[))]') def process_node(node): nonlocal total if isinstance(node, dict): for key, value in node.items(): # 检查键名中是否包含 '(XX分)' 或 '(XX分)' match = score_pattern.search(key) if match: score = int(match.group(1)) total += score # 匹配到后,不再递归处理其子项 continue elif key == '评分': if isinstance(value, str): # 提取 '评分' 键的值中的数字,如 '20分' match_score = re.match(r'(\d+)分', value) if match_score: score = int(match_score.group(1)) total += score else: # 如果 '评分' 值不符合格式,默认加 0 total += 0 elif isinstance(value, int): # 如果 '评分' 键的值是整数,直接累加 total += value else: # 如果 '评分' 键的值既不是字符串也不是整数,默认加 0 total += 0 else: # 如果键名不包含分数,递归处理其子项 process_node(value) elif isinstance(node, list): for item in node: process_node(item) else: # 如果是其他类型的数据,忽略 pass # 开始递归处理外层键的值 process_node(outer_value) return total def reorganize_data(input_dict, include=None): """ 重组输入字典,将“技术评分”和“商务评分”提升为最外层键, 并将包含在 include 列表中的包名的数据嵌套在相应的评分类别下。 如果 input_dict 的顶层键不包含任何 include 列表中的项,则返回原始字典。 :param input_dict: 原始输入字典 :param include: 包名列表,例如 ['一包', '二包', '三包'] :return: 重组后的字典 """ if include is None: include = [] # 检查是否有任何顶层键包含在 include 列表中 has_include = any(key in include for key in input_dict.keys()) if not has_include: # 没有包含任何指定的包名,直接返回原始字典 return input_dict # 初始化新的字典结构 reorganized = { "技术评分": {}, "商务评分": {} } # 遍历每一个包(如 "一包", "二包") for package, categories in input_dict.items(): # 处理技术评分 if "技术评分" in categories: reorganized["技术评分"][package] = categories["技术评分"] # 处理商务评分 if "商务评分" in categories: reorganized["商务评分"][package] = categories["商务评分"] return reorganized # 格式要求: # 请以 JSON 格式返回结果,最外层键名为 '技术评分'、'商务评分' 和 '投标报价评分'。在每大项下,用键值对表示具体评分项,键为具体的评审因素,若评审因素存在嵌套(表格中存在层级),请使用嵌套键值对表示,具体规则如下: # 1. 如果评审因素存在嵌套,使用嵌套键值对表示: # -主评审因素的键名后需附加括号,表示该主因素下所有子因素总分,例如:产品技术响应(8分) # -子评审因素作为主评审因素的内层键名 # 2. 如果评审因素不存在嵌套,那么键名就是该评审因素 # 3. 每个评审因素的最内层键值都是列表,列表中包含描述评分及要求的字典,字典需包含以下键: # '评分':具体得分或定性指标(如 '合格制'),无评分时可删去'评分'键值对。 # '要求':说明评分标准。 # 4.若这三大项评分中存在额外信息(不属于某个评审因素,即该大项评分的整体要求),在该评分项内部新增键名为'备注',值为该要求。 def combine_evaluation_standards(evaluation_method_path,invalid_path,zb_type): # 定义默认的评审结果字典 DEFAULT_EVALUATION_REVIEW = { "技术评分": "", "商务评分": "" } # 如果 truncate_file 是空字符串,直接返回包含空字符串的字典 if not evaluation_method_path: return DEFAULT_EVALUATION_REVIEW.copy() def run_first_query(file_path): print("判断有无评分") # 上传文件并获取文件ID file_id = upload_file(file_path) # 定义用户查询 query = ( """根据该文档,你判断它是否有关于技术评分或商务评分或投标报价的具体的评分及要求如果有,返回'是',否则返回'否'。 要求与指南: 1. 评分要求主要以表格形式呈现,且有评分因素及评分要求、标准。 2. 竞争性磋商文件通常无评分要求,但若满足'1.'的内容,也请返回'是'。 3. 仅返回'是'或'否',不需要其他解释或内容。 """ ) # 应对竞争性谈判这种无评分要求的情况 # 执行查询 return qianwen_long(file_id, query),file_id def run_second_qeury(file_id,processed_filepath,model_type): print("获取评分项...") # 执行 user_query 相关的逻辑 user_query_1 = ( """ 你是一个对招投标业务非常熟悉的专家。根据该文档中的评标办法表格,请你列出该文件的技术评分,商务评分,投标报价评分以及它们对应的具体评分要求,请以JSON格式返回结果。 格式要求: 1.总体结构: -JSON 的最外层包含三个键:技术评分、商务评分 和 投标报价评分。 -每个大项(如技术评分、商务评分)下包含具体的评分项,评分项按以下规则表示。 2.评分项表示规则: -层级嵌套规则: 若评分因素(内容)存在嵌套关系(通过表格结构判断):主评分因素需附加括号,括号中注明该主评分项的总分,若无具体评分,则无需添加该括号。例如:产品技术响应(8分);子评分因素作为嵌套键名,列在主评分因素之下,无需再附加括号表示评分。 -注意:禁止通过归纳、推测或自行总结来生成子评分因素,尤其不可根据'评分标准'中的打分要求来反向总结'子评分因素'。 若评分因素(内容)不存在嵌套关系:键名直接为评分因素,无需附加括号表示总分。 -评分项内容: -每个评分项最内层值都是列表,列表中包含描述评分标准或要求的字典。 -字典个数: 默认为1个字典,若某评分因素包括多个评分标准(多个表格单元格),可以用多个并列字典表示。 -字典结构如下: 评分:该评分标准的总分(如 8分),字符串类型;不能是一个范围数字(如0-8分);若为定性指标(如“合格制”),可标明相应的定性指标;无评分时可删去'评分'键值对。 要求:说明评分标准或要求。 -禁止情况: 禁止将同个单元格内的内容拆分至多个字典中;禁止遗漏单元格内任何信息,包括注的内容。 3.备注信息: -若评分部分包含附加信息(如大项评分的整体要求,未直接归属于具体评分项),需添加一个 备注 键,值为该附加信息。 要求与指南: 1. 请首先定位评分表,不要回答有关资格审查、符合审查的内容,也不要从评标办法正文中提取回答。 2. 若表中的评分大项不是这三个,或者有额外的评分大项: -请你根据语义及你对招投标业务的熟悉,分别映射到'技术评分'、'商务评分'、'投标报价评分',而不必按照表格中大项名称。 -映射关系可以是“一对一”或“多对一”,例如两个评分大项可以同时归类到“技术评分”。关键是不遗漏表中的评分大项,确保每个大项都被映射。 若表中只有评分因素而无评分大项: -请将评分因素正确地归类到'技术评分'、'商务评分'、'投标报价评分',不要遗漏任何一个评分因素。 3. 若表格中商务和技术评分混合一起,或者有部分评分因素无法通过表格结构明确归类: -请根据你对招投标业务的熟悉,对表格中的评分因素进行准确分类。关键是确保每个评分因素都能被归类到'技术评分'或'商务评分'或'投标报价评分',不要遗漏任何一个评分因素。 特殊情况: 1. 最外层键名为各大评分项;但是如果该招标、采购活动有多个分包,则最外层键名为对应的包名,如'一包',内部格式不变。 2. 若大项的'xx评分'要求未在文中说明,则键名'xx评分'的键值设为字符串'本项目无xx评分项',例如"技术评分":"本项目无技术评分项",而非默认的字典格式。 3. 默认情况大项评分仅有'技术评分''商务评分''投标报价评分',若在充分归类之后,表格中仍有评分因素未被归类,才添加大项评分'其他'保存该内容。 禁止内容: 1. 确保所有输出内容均基于提供的实际招标文件内容(除了最外层的三个评分大项名称),不使用任何预设的示例作为回答。 2. 不得擅自添加不属于评审因素的键名以及 `'备注'` 之外的其他键名。 3. 不得遗漏评分表中的任何评分因素,确保每个评分因素都被正确归类到评分大项下。 以下为示例输出,仅供格式参考: { "一包": { "技术评分": { "实施方案(16分)":{ "总体实施方案":[ { "评分":8, "要求":"根据投标人总体实施方案进行评分" } ], "项目实施要点":[ { "评分":8, "要求":"根据投标人对项目实施要点、难点进行评分。" } ] }, "设计创意": [ { "评分": "10分", "要求": "主题突出,形式多样,内容与形式完美统一,得10分,其他酌情打分。" } ], "备注": "技术标采用暗标形式,暗标不得出现投标人名称、人员姓名。" }, "商务评分": { "主要监理岗位的职责": [ { "评分": "4分", "要求": "1、总监理工程师的职责全面、清晰、合理得 2 分;一般的 1 分。2、其他主要监理人员及岗位的职责全面、清晰、合理得 2 分;一般的 1 分。" } ], "制造商实力": [ { "评分": "3分", "要求": "一级证书得3分,二级证书得1分,其他不得分。" }, { "评分": "2分", "要求": "行业销量排名连续前 2 名,得 2 分,第 3-6 名得 0.5 分,其他不得分。" } ] }, "投标报价评分": { "投标报价是否出现违反计价规范": [ { "评分": "合格制", "要求": "A:投标报价未违反计价规范的,评审意见为“合格”;B:投标报价违反计价规范的,评审意见为“不合格”" } ] } } } """ ) user_query_2 = ( """ 你是一个对招投标业务非常熟悉的专家。根据该文档中的评标办法表格,请你列出该文件的技术评分,商务评分,投标报价评分以及它们对应的具体评分要求,请以JSON格式返回结果。 格式要求: 1.总体结构: -JSON 的最外层包含三个键:'技术评分'、'商务评分' 和 '投标报价评分'。 -每个大项(如技术评分、商务评分)下包含具体的评分项,评分项按以下规则表示。 2.评分项表示规则: -层级嵌套规则: 若评分因素(内容)存在嵌套关系(通过表格结构判断):主评分因素需附加括号,括号中注明该主评分项的总分,若无具体评分,则无需添加该括号。例如:产品技术响应(8分);子评分因素作为嵌套键名,列在主评分因素之下,无需再附加括号表示评分。 -注意:禁止通过归纳、推测或自行总结来生成子评分因素,尤其不可根据'评分标准'中的打分要求来反向总结'子评分因素'。 若评分因素(内容)不存在嵌套关系:键名直接为评分因素,无需附加括号表示总分。 -评分项内容: -每个评分项最内层值都是列表,列表中包含描述评分标准或要求的字典。 -字典个数: 默认为1个字典,若某评分因素包括多个评分标准(多个表格单元格),可以用多个并列字典表示。 -字典结构如下: 评分:该评分标准的总分(如 8分),字符串类型;不能是一个范围数字(如0-8分);若为定性指标(如“合格制”),可标明相应的定性指标;无评分时可删去'评分'键值对。 要求:说明评分标准或要求。 -禁止情况: 禁止将同个单元格内的内容拆分至多个字典中;禁止遗漏单元格内任何信息,包括注的内容。 3.备注信息: -若评分部分包含附加信息(如大项评分的整体要求,未直接归属于具体评分项),需添加一个 '备注' 键,值为该附加信息。 要求与指南: 1. 请首先定位评分表,不要回答有关资格审查、符合审查的内容,也不要从评标办法正文中提取回答。 2. 若表中的评分大项不是这三个,或者有额外的评分大项: -请你根据语义及你对招投标业务的熟悉,分别映射到'技术评分'、'商务评分'、'投标报价评分',而不必按照表格中大项名称。 -映射关系可以是“一对一”或“多对一”,例如两个评分大项可以同时归类到“技术评分”。关键是不遗漏表中的评分大项,确保每个大项都被映射。 若表中只有评分因素而无评分大项: -请将评分因素正确地归类到'技术评分'、'商务评分'、'投标报价评分',不要遗漏任何一个评分因素。 3. 若表格中商务和技术评分混合一起,或者有部分评分因素无法通过表格结构明确归类: -请根据你对招投标业务的熟悉,对表格中的评分因素进行准确分类。关键是确保每个评分因素都能被归类到'技术评分'或'商务评分'或'投标报价评分',不要遗漏任何一个评分因素。 特殊情况: 1. 最外层键名为各大评分项;但是如果该招标、采购活动有多个分包,则最外层键名为对应的包名,如'一包',内部格式不变。 2. 若大项的'xx评分'要求未在文中说明,则键名'xx评分'的键值设为字符串'本项目无xx评分项',例如"技术评分":"本项目无技术评分项",而非默认的字典格式。 3. 默认情况大项评分仅有'技术评分''商务评分''投标报价评分',若在充分归类之后,表格中仍有评分因素未被归类,才添加大项评分'其他'保存该内容。 禁止内容: 1. 确保所有输出内容均基于提供的实际招标文件内容(除了最外层的三个评分大项名称),不使用任何预设的示例作为回答。 2. 不得擅自添加不属于评审因素的键名以及 `'备注'` 之外的其他键名。 以下为示例输出,仅供格式参考: { "一包": { "技术评分": { "产品技术响应(8分)":{ "常规参数符合":[ { "评分":"4分", "要求":"未标★项为常规参数每条负偏离扣1分,本项满分4分。" } ], "控制系统":[ { "评分":"4分", "要求":"所投电梯控制系统技术先进、市场美誉度高,与整梯同品牌,得4分;所投电梯控制系统的技术基本先进、市场美誉度较高,与整梯不同品牌,得2分。" } ] }, "实施方案": [ { "评分": "10分", "要求": "实施方案清晰、完整、合理、可行的得 10 分。实施方案一般的得5分" } ], "备注": "注:若不满足“与公安部、省公安厅、随州市公安局高清视频会议系统无缝对接互联互通”的要求,则本项技术部分不得分。" }, "商务评分": { "主要监理岗位的职责": [ { "评分": "4分", "要求": "1、总监理工程师的职责全面、清晰、合理得 2 分;一般的 1 分。2、其他主要监理人员及岗位的职责全面、清晰、合理得 2 分;一般的 1 分。" } ], "制造商实力": [ { "评分": "3分", "要求": "一级证书得3分,二级证书得1分,其他不得分" }, { "评分": "2分", "要求": "行业销量排名连续前 2 名,得 2 分,第 3-6 名得 0.5 分,其他不得分" } ] }, "投标报价评分": { "投标报价是否出现违反计价规范": [ { "评分": "合格制", "要求": "A:投标报价未违反计价规范的,评审意见为“合格”;B:投标报价违反计价规范的,评审意见为“不合格”" } ] } } } """ ) # 执行第二个查询 user_query = user_query_1 if zb_type == 1 else user_query_2 if model_type==4: full_text = read_txt_to_string(processed_filepath) user_query += f"\n文件内容:\n{full_text}\n" questions_to_continue = [] temp_final={} if model_type==4: # evaluation_res=doubao_model(user_query,True) evaluation_res = qianwen_plus(user_query, True) else: evaluation_res = qianwen_long(file_id, user_query,2,1,True) # 有些重复的键名,只有qianwen_long_text能保留 message = evaluation_res[0] # print(message) total_tokens = evaluation_res[1] # print(evaluation_res) # 清理和处理响应 cleaned_evaluation_res = extract_content_from_json(message,True) # 带上True处理重复键名的情况-》生成列表 # print(json.dumps(cleaned_evaluation_res,ensure_ascii=False,indent=4)) max_tokens = 7900 if model_type == 4 else 5900 if not cleaned_evaluation_res and total_tokens > max_tokens: questions_to_continue.append((user_query, evaluation_res)) else: temp_final.update(cleaned_evaluation_res) if questions_to_continue: continued_results = process_continue_answers(questions_to_continue, model_type, file_id) temp_final.update(continued_results) result_data = process_data_based_on_key(temp_final) # 处理不知名外键的情况 include = ['一包', '二包', '三包', '四包', '五包'] updated_jsons = {} # 检查是否有外层键匹配 include 列表 if any(key for key in result_data if any(included in key for included in include)): # 检查result_data中的任何键是否包含include列表中的任意一个项。 # 有匹配的项,处理这些项 for key in result_data: if any(item in key for item in include): inner_dict = result_data[key] updated_jsons[key] = combine_technical_and_business(inner_dict) # 对于分包,单独对分包内的'技术评分''商务评分'作处理 else: # 没有匹配的项,对整个字典运行 updated_jsons = combine_technical_and_business(result_data) final_res = reorganize_data(updated_jsons, include) # 重新组织字典,尤其是分包的情况 return final_res try: judge_res,file_id = run_first_query(evaluation_method_path) # 检查 judge_res 的内容 def get_default_result(): return { '技术评分': '本招标文件没有技术评分!', '商务评分': '本招标文件没有商务评分!' } # 如果 judge_res 包含 '是',直接运行第二步查询 if '是' in judge_res: if get_pdf_page_count(evaluation_method_path)<=20: model_type=4 #qianwen-plus processed_filepath = convert_file_to_markdown(evaluation_method_path,"extract2.txt") # 转markdown格式 else: evaluation_method_docxpath=pdf2docx(evaluation_method_path) file_id=upload_file(evaluation_method_docxpath) model_type = 2 #qianwen-long processed_filepath = "" return run_second_qeury(file_id,processed_filepath,model_type) # 标准化路径,避免多种表示形式造成的误判 eval_path = os.path.abspath(evaluation_method_path) invalid_eval_path = os.path.abspath(invalid_path) # 判断路径是否一致 if eval_path == invalid_eval_path: # 如果路径一致,直接返回默认结果 return get_default_result() # 路径不一致,尝试运行第一次查询 judge_res, file_id = run_first_query(invalid_path) # 如果 judge_res 包含 '是',运行第二步查询 if '是' in judge_res: model_type = 0 processed_filepath = "" return run_second_qeury(file_id, processed_filepath, model_type) # 默认返回结果 return get_default_result() except Exception as e: print(f"Error in combine_evaluation_standards: {e}") # 在出错时返回默认的包含空字符串的字典 return DEFAULT_EVALUATION_REVIEW.copy() #目前评分这块如果表格过长,会有问题,可以考虑textin+doubao,小于20页用text,>20页转word->qianwen-long #TODO:代码计算总分,商务评分修改 #TODO:废标项,增加对表格的提取+排除重复项 if __name__ == "__main__": start_time=time.time() # truncate_file=r"C:\Users\Administrator\Desktop\招标文件-采购类\tmp2\2024-新疆-塔城地区公安局食药环分局快检实验室项目_evaluation_method.pdf" evaluation_method_path = r'D:\flask_project\flask_app\static\output\output1\f91db70d-8d96-44a5-b840-27d2f1ecbe95\invalid_del.docx' invalid_path=r'D:\flask_project\flask_app\static\output\output1\f91db70d-8d96-44a5-b840-27d2f1ecbe95\invalid_del.docx' # truncate_file = "C:\\Users\\Administrator\\Desktop\\货物标\\output2\\2-招标文件(统计局智能终端二次招标)_evaluation_method.pdf" # truncate_file="C:\\Users\\Administrator\\Desktop\\货物标\\output2\\广水市妇幼招标文件最新(W改)_evaluation_method.pdf" # truncate_file = "C:\\Users\\Administrator\\Desktop\\fsdownload\\2d481945-1f82-45a5-8e56-7fafea4a7793\\ztbfile_evaluation_method.pdf" # truncate_file="C:\\Users\\Administrator\\Desktop\\fsdownload\\ztbfile_evaluation_method.pdf" res = combine_evaluation_standards(evaluation_method_path,invalid_path,2) print(json.dumps(res, ensure_ascii=False, indent=4)) end_time=time.time() print("elapsed time:"+str(end_time-start_time))