diff --git a/.gitignore b/.gitignore index 1ed5d90..23b8124 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,7 @@ flask_app/static/output/ *.rar # 忽略旧版本代码 -flask_app/old_version/ +#flask_app/old_version/ # 忽略操作系统特定的文件 .DS_Store diff --git a/flask_app/general/file2markdown.py b/flask_app/general/file2markdown.py index 271fc6c..ba858d8 100644 --- a/flask_app/general/file2markdown.py +++ b/flask_app/general/file2markdown.py @@ -72,6 +72,6 @@ def convert_pdf_to_markdown(file_path): if __name__ == "__main__": - file_path=r"C:\Users\Administrator\Desktop\货物标\output3\ztbfile_qualification1.pdf" + file_path=r"C:\Users\Administrator\Desktop\fsdownload\e702f1e6-095d-443d-bb7d-ef2e42037cb1\ztbfile_procurement.pdf" res=convert_pdf_to_markdown(file_path) print(res) \ No newline at end of file diff --git a/flask_app/general/json_utils.py b/flask_app/general/json_utils.py index 8bd6f1b..1e0d7e1 100644 --- a/flask_app/general/json_utils.py +++ b/flask_app/general/json_utils.py @@ -104,7 +104,7 @@ def extract_content_from_json(string): # 尝试直接解析原始 JSON 数据 try: parsed = json.loads(original_json) - print("直接解析原始 JSON 成功。") + # print("直接解析原始 JSON 成功。") return parsed except json.JSONDecodeError as original_error: print(f"直接解析原始 JSON 失败: {original_error}") diff --git a/flask_app/general/多线程提问.py b/flask_app/general/多线程提问.py index eec33d3..32ff7d6 100644 --- a/flask_app/general/多线程提问.py +++ b/flask_app/general/多线程提问.py @@ -238,82 +238,67 @@ def pure_assistant(): return assistant def llm_call(question, knowledge_name,file_id, result_queue, ans_index, llm_type): - if llm_type==1: - print(f"rag_assistant! question:{question}") - assistant = rag_assistant(knowledge_name) - # assistant=create_assistant(knowledge_name) - elif llm_type==2: - print(f"qianwen_long! question:{question}") - # 获取当前时间 - current_time = datetime.now() - # 输出时分秒 - print(current_time.strftime("%H:%M:%S.%f")[:-3]) - # qianwen_res,usage = qianwen_long(file_id,question) #有bug - qianwen_res = qianwen_long(file_id, question) - result_queue.put((ans_index,(question,qianwen_res))) - return - elif llm_type==3: - # print(f"doubao! question:{question}") - doubao_res=doubao_model(question) - result_queue.put((ans_index, (question, doubao_res))) - return - else : - assistant = pure_assistant() - ans = send_message(assistant, message=question) - result_queue.put((ans_index, (question, ans))) # 在队列中添加索引 (question, ans) + """ + 调用不同的 LLM 模型并将结果放入结果队列。 + """ + try: + if llm_type==1: + print(f"rag_assistant! question:{question}") + assistant = rag_assistant(knowledge_name) + # assistant=create_assistant(knowledge_name) + ans = send_message(assistant, message=question) + result_queue.put((ans_index, (question, ans))) # 在队列中添加索引 (question, ans) + elif llm_type==2: + print(f"qianwen_long! question:{question}") + # qianwen_res,usage = qianwen_long(file_id,question) #有bug + qianwen_res = qianwen_long(file_id, question) + if not qianwen_res: + result_queue.put((ans_index, None)) # 如果为空字符串,直接返回 None + else: + result_queue.put((ans_index, (question, qianwen_res))) + elif llm_type==3: + # print(f"doubao! question:{question}") + doubao_res=doubao_model(question) + if not doubao_res: + result_queue.put((ans_index, None)) # 如果为空字符串,直接返回 None + else: + result_queue.put((ans_index, (question, doubao_res))) + else : + assistant = pure_assistant() + ans = send_message(assistant, message=question) + result_queue.put((ans_index, (question, ans))) # 在队列中添加索引 (question, ans) + except Exception as e: + print(f"LLM 调用失败,查询索引 {ans_index},错误:{e}") + result_queue.put((ans_index, None)) # 使用 None 作为失败的占位符 def multi_threading(queries, knowledge_name="", file_id="", llm_type=1): if not queries: return [] - print("多线程提问:starting multi_threading...") result_queue = queue.Queue() - max_retries = 2 # 设置最大重试次数 - retry_counts = {} # 跟踪每个查询的重试次数 + with concurrent.futures.ThreadPoolExecutor(max_workers=60) as executor: + future_to_index = { + executor.submit(llm_call, query, knowledge_name, file_id, result_queue, index, llm_type): index + for index, query in enumerate(queries) + } - with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor: - future_to_query = {} - for index, query in enumerate(queries): - # time.sleep(0.5) # 每提交一个任务后等待0.5秒,目前设置了直接对qianwen-long直接限制,无需sleep - future = executor.submit(llm_call, query, knowledge_name, file_id, result_queue, index, llm_type) - future_to_query[future] = index - retry_counts[index] = 0 # 初始化重试次数 + for future in concurrent.futures.as_completed(future_to_index): + index = future_to_index[future] + try: + future.result() # 确保任务完成,如果有未处理的异常会在这里抛出 + except Exception as exc: + print(f"查询索引 {index} 生成了一个异常:{exc}") + result_queue.put((index, None)) # 使用 None 作为失败的占位符 - while future_to_query: - done, _ = concurrent.futures.wait( - future_to_query.keys(), - return_when=concurrent.futures.FIRST_COMPLETED - ) - for future in done: - index = future_to_query[future] - del future_to_query[future] - try: - future.result() # 捕获异常或确认任务完成 - except Exception as exc: - print(f"Query {index} generated an exception: {exc}") - retry_counts[index] += 1 # 增加重试计数 - #Query 0 generated an exception: Error code: 429 - {'error': {'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-thousand-questions-metering-and-billing.', 'type': 'insufficient_quota', 'param': None, 'code': 'insufficient_quota'}, 'request_id': 'de10e2e9-78c2-978f-8801-862ffb0892e9'} - if retry_counts[index] <= max_retries: - print(f"Retrying query {index} (attempt {retry_counts[index]})...") - print("重试的问题:" + queries[index]) - # 重新提交任务 - new_future = executor.submit(llm_call, queries[index], knowledge_name, file_id, result_queue, index, llm_type) - future_to_query[new_future] = index - else: - print(f"Query {index} failed after {max_retries} attempts.") - result_queue.put((index, None)) # 添加占位符 - - # 从队列中获取所有结果并按索引排序 + # 初始化结果列表,确保按查询的索引顺序排列 results = [None] * len(queries) while not result_queue.empty(): index, result = result_queue.get() results[index] = result - - # 检查是否所有结果都是 None + # 可选:过滤掉所有结果为 None 的项 + # 如果希望保留 None 以表示失败的查询,可以注释掉以下代码 if all(result is None for result in results): return [] - - # 过滤掉None值 results = [r for r in results if r is not None] return results diff --git a/flask_app/general/接口_小解析.py b/flask_app/general/接口_小解析.py index 7a25baf..0fc9bda 100644 --- a/flask_app/general/接口_小解析.py +++ b/flask_app/general/接口_小解析.py @@ -12,7 +12,7 @@ from flask_app.货物标.基础信息解析main import aggregate_basic_info_good from flask_app.货物标.截取pdf货物标版 import truncate_pdf_specific_goods from flask_app.main.截取pdf import truncate_pdf_specific_engineering,truncate_pdf_main from flask_app.general.post_processing import inner_post_processing -from flask_app.old_version.基础信息整合 import aggregate_basic_info_engineering +from flask_app.old_version.基础信息整合_old import aggregate_basic_info_engineering def get_global_logger(unique_id): diff --git a/flask_app/general/接口_技术偏离表.py b/flask_app/general/接口_技术偏离表.py index 42e682f..c3efbe3 100644 --- a/flask_app/general/接口_技术偏离表.py +++ b/flask_app/general/接口_技术偏离表.py @@ -174,38 +174,40 @@ def extract_business_deviation(procurement): business_requirements_string = json.dumps(new_data, ensure_ascii=False, indent=4) # print(business_requirements_string) - prompt_template1 = """请帮我从以下文本中摘取商务要求部分,并将信息重新组织,外键名为'商务要求',键值为字符串列表,其中每个字符串为一条商务要求,去除开头的序号(若有)。 -#角色 -你是一个专业的招投标业务专家,擅长从招标文件中总结商务要求的部分,并逐条列出,作为编写商务要求偏离表的前置准备。 + prompt_template1 = """请帮我从以下文本中摘取商务要求部分,并将信息重新组织,外键名为'商务要求',键值为字符串列表,其中每个字符串为一条商务要求,保留三角▲、五角星★(若有),但是去除开头的序号(若有)。 + #角色 + 你是一个专业的招投标业务专家,擅长从招标文件中总结商务要求的部分,并逐条列出,作为编写商务要求偏离表的前置准备。 -#要求与指南: -1. 每条内容需要有实际的含义、要求,不能光有标题性质的表述如'售后服务期限(质保期)及要求'。 -2. 你的回答内容需从所给文本中整理,尽量不改变原文的表达,对于嵌套键值对,若键值本身符合'商务要求',可直接将其返回;若键值本身语义表达不完整,可将键值对拼接之后作为一条商务要求,拼接符号可以是冒号,即':'。 -3. 若无商务要求,键值为空列表,即[] + #要求与指南: + 1. 每条内容需要有实际的含义、要求,不能光有标题性质的表述如'售后服务期限(质保期)及要求'。 + 2. 你的回答内容需从所给文本中整理,尽量不改变原文的表达,请勿擅自添加三角▲、五角星★;若输入文本中存在嵌套键值对格式,且键值本身符合'商务要求',可直接将其添加至'商务要求'的键值中;若键值本身语义表达不完整,可将键值对拼接之后作为一条商务要求,拼接符号可以是冒号,即':'。 + 3. 输入文本中出现三角▲或五角星★开头的地方,请格外重视,不可漏提,若其后面的内容是标题性质的表述、不具备实际的要求,请你保留三角▲或五角星★,根据语义添加至紧跟着的字符串开头中。 + 3. 若无商务要求,键值为空列表,即[] -### 示例输入如下: -{{ - "招标要求1": "整个平台运行运维服务,须安排人员驻场对平台进行运行维护,采用 4人轮流值班,依照 7×12小时对可视化督察巡控平台进行操作,确保平台稳定运行,并对线上发现违规操作进行记录,通过督察平台推送督办单给线下监督员小程序进行检查。" - "招标要求2": {{ - "合同履行期限": "交货期(工期):合同签订之日起 15个日历天内完成,并通过项目验收。", - "交货地点": "采购人指定地点", - "报价方式": "本项目报价须为固定总价,包含但不限于:采购、实施、调试、试运行、验收、运维等所有完成本项目相关的一切费用。", - "其他要求": "无。" - }}, - "招标要求3": "路口必须在各方向埋设双管。" -}} -### 对应的参考输出如下: -{{ - "商务要求":[ - "整个平台运行运维服务,须安排人员驻场对平台进行运行维护,采用 4人轮流值班,依照 7×12小时对可视化督察巡控平台进行操作,确保平台稳定运行,并对线上发现违规操作进行记录,通过督察平台推送督办单给线下监督员小程序进行检查。", - "交货期(工期):合同签订之日起 15个日历天内完成,并通过项目验收。", - "交货地点:采购人指定地点", - "本项目报价须为固定总价,包含但不限于:采购、实施、调试、试运行、验收、运维等所有完成本项目相关的一切费用。" - ] -}} -文本内容:{full_text} -""" + ### 示例输入如下: + {{ + "招标要求1": ["▲(1)整个平台运行运维服务,须安排人员驻场对平台进行运行维护,采用 4人轮流值班,依照 7×12小时对可视化督察巡控平台进行操作,确保平台稳定运行,并对线上发现违规操作进行记录,通过督察平台推送督办单给线下监督员小程序进行检查。"] + "招标要求2": {{ + "合同履行期限": ["★交货期(工期):合同签订之日起 15个日历天内完成,并通过项目验收。"], + "交货地点": ["采购人指定地点"], + "报价方式": ["(1)本项目报价须为固定总价,包含但不限于:采购、实施、调试、试运行、验收、运维等所有完成本项目相关的一切费用。","(2)因投标人自身原因造成漏报、少报皆由其自行承担责任,采购人不再补偿。"], + "其他要求": ["无。"] + }} + }} + ### 对应的参考输出如下: + {{ + "商务要求":[ + "▲整个平台运行运维服务,须安排人员驻场对平台进行运行维护,采用 4人轮流值班,依照 7×12小时对可视化督察巡控平台进行操作,确保平台稳定运行,并对线上发现违规操作进行记录,通过督察平台推送督办单给线下监督员小程序进行检查。", + "★交货期(工期):合同签订之日起 15个日历天内完成,并通过项目验收。", + "交货地点:采购人指定地点", + "本项目报价须为固定总价,包含但不限于:采购、实施、调试、试运行、验收、运维等所有完成本项目相关的一切费用。", + "因投标人自身原因造成漏报、少报皆由其自行承担责任,采购人不再补偿。" + ] + }} + + 文本内容:{full_text} + """ user_query1 = prompt_template1.format(full_text=business_requirements_string) model_res1 = doubao_model(user_query1) # print(model_res) diff --git a/flask_app/general/清除file_id.py b/flask_app/general/清除file_id.py index cab29ba..089f95b 100644 --- a/flask_app/general/清除file_id.py +++ b/flask_app/general/清除file_id.py @@ -8,18 +8,75 @@ client = OpenAI( base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", ) -# 获取文件列表 -file_stk = client.files.list() -# 将文件信息解析为 JSON 格式 -file_data = json.loads(file_stk.model_dump_json()) +def delete_all_files(): + """ + 查询所有文件并删除。 + """ + try: + # 获取文件列表 + file_stk = client.files.list() -# 提取所有文件的 id -file_ids = [file["id"] for file in file_data["data"]] + # 将文件信息解析为 JSON 格式 + file_data = json.loads(file_stk.model_dump_json()) -# num=len(file_ids) -# print(num) -# 循环删除每个文件 -for file_id in file_ids: - file_object = client.files.delete(file_id) - print(file_object.model_dump_json()) + # 提取所有文件的 id + file_ids = [file["id"] for file in file_data["data"]] + + # 循环删除每个文件 + for file_id in file_ids: + file_object = client.files.delete(file_id) + print(f"Deleted file with id: {file_id} - {file_object.model_dump_json()}") + + except Exception as e: + print(f"An error occurred while deleting files: {e}") + + +def read_file_ids(output_folder): + file_ids = [] + file_ids_path = os.path.join(output_folder, 'file_ids.txt') + # 检查文件是否存在 + if os.path.exists(file_ids_path): + try: + with open(file_ids_path, 'r', encoding='utf-8') as file: + # 按行读取文件内容 + file_ids = [line.strip() for line in file.readlines()] + print(f"读取到的文件 ID 列表:{file_ids}") + except Exception as e: + print(f"读取 file_ids.txt 文件时发生错误: {e}") + else: + print(f"文件 {file_ids_path} 不存在。") + + return file_ids + + +def delete_file_by_ids(file_ids): + """ + 根据传入的 file_id 列表删除指定的文件。 + + :param file_ids: 一个包含文件 ID 的字符串列表 + """ + if not isinstance(file_ids, list): + print("Error: file_ids should be a list.") + return + + if not all(isinstance(file_id, str) for file_id in file_ids): + print("Error: Each file_id should be a string.") + return + + try: + # 删除指定文件 + for file_id in file_ids: + try: + # 假设 client.files.delete 会返回一个文件对象 + file_object = client.files.delete(file_id) + # print(f"Deleted file with id: {file_id} - {file_object.model_dump_json()}") + except Exception as e: + # 处理删除单个文件时的异常 + print(f"Failed to delete file with id {file_id}: {e}") + + except Exception as e: + print(f"An error occurred while processing the file_ids: {e}") + +if __name__ == '__main__': + delete_all_files() \ No newline at end of file diff --git a/flask_app/general/通义千问long.py b/flask_app/general/通义千问long.py index 01c6b9d..67cfbe8 100644 --- a/flask_app/general/通义千问long.py +++ b/flask_app/general/通义千问long.py @@ -1,6 +1,10 @@ +import ast import json +import re +import threading from functools import wraps +from flask import g from ratelimit import limits, sleep_and_retry import random import time @@ -8,16 +12,35 @@ from pathlib import Path from openai import OpenAI import os -def upload_file(file_path): +file_write_lock = threading.Lock() +def upload_file(file_path,output_folder=""): """ Uploads a file to DashScope and returns the file ID. + Additionally, saves the file ID to 'file_ids.txt' in the given output folder. """ + if not output_folder: + output_folder=os.path.dirname(file_path) client = OpenAI( api_key=os.getenv("DASHSCOPE_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) + + # 上传文件并获取 file_id file = client.files.create(file=Path(file_path), purpose="file-extract") - return file.id + file_id = file.id + + # 创建output_folder路径,如果它不存在 + if not os.path.exists(output_folder): + os.makedirs(output_folder) + + # 确保文件写入是互斥的 + with file_write_lock: # 在这个代码块中,其他线程无法进入 + file_ids_path = os.path.join(output_folder, 'file_ids.txt') + # 如果文件不存在,就创建它并写入 file_id + with open(file_ids_path, 'a') as f: + f.write(f'{file_id}\n') + + return file_id @sleep_and_retry @limits(calls=4, period=1) # 每秒最多调用4次 @@ -32,95 +55,178 @@ def shared_rate_limit(func): return func(*args, **kwargs) return wrapper @shared_rate_limit -def qianwen_long(file_id, user_query): - print("call qianwen-long...") +def qianwen_long(file_id, user_query, max_retries=2, backoff_factor=1.0): """ - Uses a previously uploaded file to generate a response based on a user query. + 基于上传的文件 ID 和用户查询生成响应,并在失败时自动重试。 + 参数: + - file_id: 上传文件的 ID + - user_query: 用户查询 + - max_retries: 最大重试次数(默认 2 次) + - backoff_factor: 指数退避的基础等待时间(默认 1.0 秒) """ + print("call qianwen_long...") + client = OpenAI( api_key=os.getenv("DASHSCOPE_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) - # Generate a response based on the file ID - completion = client.chat.completions.create( - model="qwen-long", - # top_p=0.5, - temperature=0.5, - # response_format={"type":"json_object"}, - messages=[ - { - 'role': 'system', - 'content': f'fileid://{file_id}' - }, - { - 'role': 'user', - 'content': user_query - } - ], - stream=False - ) + for attempt in range(1, max_retries + 2): # +1 是为了包括初始调用 + try: + # 调用 API + completion = client.chat.completions.create( + model="qwen-long", + temperature=0.5, + messages=[ + { + 'role': 'system', + 'content': f'fileid://{file_id}' + }, + { + 'role': 'user', + 'content': user_query + } + ], + stream=False + ) + # 如果调用成功,返回响应内容 + return completion.choices[0].message.content + + except Exception as exc: + # 提取错误代码 + error_code, error_code_string = extract_error_details(str(exc)) + print(f"第 {attempt} 次尝试失败,查询:'{user_query}',错误:{exc}") + + if error_code == 429: + if attempt <= max_retries: + sleep_time = backoff_factor * (2 ** (attempt - 1)) # 指数退避 + print(f"错误代码为 429,将在 {sleep_time} 秒后重试...") + time.sleep(sleep_time) + else: + print(f"查询 '{user_query}' 的所有 {max_retries + 1} 次尝试均失败(429 错误)。") + elif error_code == 400 and error_code_string in ['data_inspection_failed', 'ResponseTimeout','DataInspectionFailed','response_timeout']: + if attempt == 1: # 只重试一次 + print(f"错误代码为 400 - {error_code_string},将立即重试...") + continue # 直接跳到下一次循环(即重试一次) + else: + print(f"查询 '{user_query}' 的所有 {max_retries + 1} 次尝试均失败(400 - {error_code_string})。") + + else: + # 对于非 429 和非特定 400 错误,不进行重试,直接抛出异常 + print(f"遇到非 429 或非 'data_inspection_failed' 的 400 错误(错误代码:{error_code}),不进行重试。") + return "" + + +def extract_error_details(error_message): + """ + 从错误消息中提取错误代码和内部错误代码。 + 假设错误消息的格式包含 'Error code: XXX - {...}' + """ + # 提取数值型错误代码 + error_code_match = re.search(r'Error code:\s*(\d+)', error_message) + error_code = int(error_code_match.group(1)) if error_code_match else None + + # 提取内部错误代码字符串(如 'data_inspection_failed') + error_code_string = None + error_dict_match = re.search(r'Error code:\s*\d+\s*-\s*(\{.*\})', error_message) + if error_dict_match: + error_dict_str = error_dict_match.group(1) + try: + # 使用 ast.literal_eval 解析字典字符串 + error_dict = ast.literal_eval(error_dict_str) + error_code_string = error_dict.get('error', {}).get('code') + print(error_code_string) + except Exception as e: + print(f"解析错误消息失败: {e}") + + return error_code, error_code_string + - # Return the response content - # return completion.choices[0].message.content,completion.usage - return completion.choices[0].message.content @shared_rate_limit -def qianwen_long_stream(file_id, user_query): - print("调用 qianwen-long stream...") +def qianwen_long_stream(file_id, user_query, max_retries = 2, backoff_factor = 1.0): """ 使用之前上传的文件,根据用户查询生成响应,并实时显示流式输出。 + 参数: + - file_id: 上传文件的 ID + - user_query: 用户查询 + - max_retries: 最大重试次数(默认 2 次) + - backoff_factor: 指数退避的基础等待时间(默认 1.0 秒) + 返回: + - Optional[str]: 成功时返回响应内容,失败时返回空字符串 """ + print("调用 qianwen-long stream...") + client = OpenAI( api_key=os.getenv("DASHSCOPE_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) - # 生成基于文件ID的响应 - completion = client.chat.completions.create( - model="qwen-long", - temperature=0.4, - messages=[ - { - 'role': 'system', - 'content': f'fileid://{file_id}' - }, - { - 'role': 'user', - 'content': user_query - } - ], - stream=True # 启用流式响应 - ) + for attempt in range(1, max_retries + 2): # +1 是为了包括初始调用 + try: + # 生成基于文件ID的响应 + completion = client.chat.completions.create( + model="qwen-long", + temperature=0.4, + max_tokens=5000, + messages=[ + { + 'role': 'system', + 'content': f'fileid://{file_id}' + }, + { + 'role': 'user', + 'content': user_query + } + ], + stream=True # 启用流式响应 + ) - full_response = "" # 用于存储完整的响应内容 + full_response = "" # 用于存储完整的响应内容 - try: - for chunk in completion: - # 假设chunk是一个对象,先将其转换为字典 - if hasattr(chunk, 'to_dict'): - chunk_data = chunk.to_dict() + for chunk in completion: + if hasattr(chunk, 'to_dict'): + chunk_data = chunk.to_dict() + else: + chunk_data = json.loads(chunk.model_dump_json()) + choices = chunk_data.get('choices', []) + if not choices: + continue + choice = choices[0] + delta = choice.get('delta', {}) + content = delta.get('content', '') + if content: + full_response += content + # print(content, end='', flush=True) # 实时打印内容 + if choice.get('finish_reason'): + break + + return full_response # 返回完整的响应内容 + + except Exception as exc: + # 提取错误代码 + error_code, error_code_string = extract_error_details(str(exc)) + print(f"第 {attempt} 次尝试失败,查询:'{user_query}',错误:{exc}") + + if error_code == 429: + if attempt <= max_retries: + sleep_time = backoff_factor * (2 ** (attempt - 1)) # 指数退避 + print(f"错误代码为 429,将在 {sleep_time} 秒后重试...") + time.sleep(sleep_time) + else: + print(f"查询 '{user_query}' 的所有 {max_retries + 1} 次尝试均失败(429 错误)。") + elif error_code == 400 and error_code_string in ['data_inspection_failed', 'ResponseTimeout', + 'DataInspectionFailed', 'response_timeout']: + if attempt == 1: # 只重试一次 + print(f"错误代码为 400 - {error_code_string},将立即重试...") + continue # 直接跳到下一次循环(即重试一次) + else: + print(f"查询 '{user_query}' 的所有 {max_retries + 1} 次尝试均失败(400 - {error_code_string})。") else: - # 如果没有to_dict方法,尝试直接转换为JSON - chunk_data = json.loads(chunk.model_dump_json()) - # 检查是否有'choices'字段 - choices = chunk_data.get('choices', []) - if not choices: - continue # 如果没有choices,跳过当前chunk - choice = choices[0] # 获取第一个choice - delta = choice.get('delta', {}) - # 提取'content'字段 - content = delta.get('content', '') - if content: - # print(content, end='', flush=True) # 实时打印内容 - full_response += content - # 检查是否有结束条件 - if choice.get('finish_reason'): - break - except KeyboardInterrupt: - print("\n中断流式响应。") - except Exception as e: - print(f"\n处理流式响应时出错: {e}") - return full_response # 返回完整的响应内容 + # 对于非 429 和非特定 400 错误,不进行重试,直接抛出异常 + print(f"遇到非 429 或非 'data_inspection_failed' 的 400 错误(错误代码:{error_code}),不进行重试。") + + # 如果所有尝试都失败了,返回空字符串 + return "" @shared_rate_limit def qianwen_long_text(file_id, user_query): diff --git a/flask_app/main/商务评分技术评分整合.py b/flask_app/main/商务评分技术评分整合.py index 1a112ae..6a785d3 100644 --- a/flask_app/main/商务评分技术评分整合.py +++ b/flask_app/main/商务评分技术评分整合.py @@ -113,9 +113,16 @@ def combine_evaluation_standards(evaluation_method): # user_query_2 = ( # "根据该文档中的评标办法前附表,请你列出该文件的技术评分,商务评分,投标报价评审标准以及它们对应的具体评分要求,若对应内容中存在其他信息,在键名如'技术评分'中新增子键名'备注'存放该信息。如果评分内容(因素)不是这3个,则返回文档中给定的评分内容(因素)以及它的评分要求。请以json格式返回结果,不要回答有关形式、资格、响应性评审标准的内容") user_query_2 = ( - """ -根据该文档中的评标办法前附表,请你列出该文件的技术评分,商务评分,投标报价评审以及它们对应的具体评分要求,请以json格式返回结果,最外层键名分别是'技术评分','商务评分','投标报价评审',请在这三大项评分中分别用若干键值对表示具体评分项,外层键名为各评审因素,可能存在嵌套关系,但最内层键值为一个列表,列表中包含若干(可为一)描述该评审因素的评分及要求的字典,内层键名分别是'评分'和'要求',若无评分,可删去'评分'键值对,'要求'中说明了该评审因素的评分标准;若这三大项评分中存在其他信息,则在相应评分大块内部新增键名'备注'存放该信息,键值为具体的要求,否则不需要。如果评分内容(因素)不是这三大项,则返回文档中给定的评分内容(因素)以及它们的具体评分要求。 -以下为需要考虑的注意事项:1.不要回答有关资格审查的内容,也不要从评标办法正文中提取回答 2.若大项的'xx评分'要求未在文中说明,则键名'xx评分'的键值设为'本项目无xx评分项',例如"技术评分":"本项目无技术评分项" 3. 如果该招标活动有多个包,则最外层键名为对应的包名,否则不需要 4.你无需将表格的单元格内的内容进行拆分,需要将它视为一个整体。以下为示例输出,仅供格式参考: + """根据该文档中的评标办法表格,请你列出该文件的技术评分,商务评分,投标报价评审以及它们对应的具体评分要求,请以json格式返回结果,最外层键名分别是'技术评分','商务评分','投标报价评审',请在这三大项评分中分别用若干键值对表示具体评分项,外层键名为各评审因素,可能存在嵌套关系,但最内层键值为一个列表,列表中包含若干(可为一)描述该评审因素的评分及要求的字典,内层键名分别是'评分'和'要求',若无评分,可删去'评分'键值对,'要求'中说明了该评审因素的评分标准;若这三大项评分中存在其他信息,则在相应评分大块内部新增键名'备注'存放该信息,键值为具体的要求,否则不需要。如果评分内容(因素)不是这三大项,则返回文档中给定的评分内容(因素)以及它们的具体评分要求。 + +要求与指南: +1. 请首先定位评分细则的表格,不要回答有关资格审查的内容,也不要从评标办法正文中提取回答 +2. 若大项的'xx评分'要求未在文中说明,则键名'xx评分'的键值设为'本项目无xx评分项',例如"技术评分":"本项目无技术评分项" +3. 如果该招标活动有多个包,则最外层键名为对应的包名,否则最外层键名为各大评分项 +4. 你无需将表格的单元格内的内容进行拆分,需要将它视为一个整体 +5. '评分'的键值不能是一个范围数字,如'0-5分',应该是一个具体数字,如'5分',或者是一个定性的指标如'合格制' + +以下为示例输出,仅供格式参考: { "一包": { "技术评分": { @@ -181,7 +188,7 @@ def combine_evaluation_standards(evaluation_method): return update_json #商务标技术标整合 if __name__ == "__main__": # evaluation_method="C:\\Users\\Administrator\\Desktop\\招标文件\\招标01_evaluation_method.pdf" - evaluation_method= r"D:\flask_project\flask_app\static\output\output1\9a4e3bc3-8367-4529-87db-af0ea53dc348\ztbfile_evaluation_method.pdf" + evaluation_method= r"C:\Users\Administrator\Desktop\招标文件\招标04_evaluation_method.pdf" evaluation_standards_res=combine_evaluation_standards(evaluation_method) # 从结果中提取"商务标"和"技术标" technical_standards = {"技术评分": evaluation_standards_res.get("技术评分", {})} diff --git a/flask_app/old_version/不得存在及禁止投标情形.py b/flask_app/old_version/不得存在及禁止投标情形_old.py similarity index 100% rename from flask_app/old_version/不得存在及禁止投标情形.py rename to flask_app/old_version/不得存在及禁止投标情形_old.py diff --git a/flask_app/general/判断截取位置.py b/flask_app/old_version/判断截取位置_old.py similarity index 99% rename from flask_app/general/判断截取位置.py rename to flask_app/old_version/判断截取位置_old.py index e4855c2..c0b1390 100644 --- a/flask_app/general/判断截取位置.py +++ b/flask_app/old_version/判断截取位置_old.py @@ -216,7 +216,7 @@ def process_string_list(string_list): def main(): # 定义JSON文件路径 # json_path = "flask_app/general/static/插入位置.json" - json_path = "D:\\flask_project\\flask_app\\general\\static\\插入位置.json" + json_path = "/flask_app/general/static/插入位置.json" # 加载数据 data_dict = load_data(json_path) diff --git a/flask_app/old_version/回答来源.py b/flask_app/old_version/回答来源_old.py similarity index 100% rename from flask_app/old_version/回答来源.py rename to flask_app/old_version/回答来源_old.py diff --git a/flask_app/old_version/基础信息整合.py b/flask_app/old_version/基础信息整合_old.py similarity index 100% rename from flask_app/old_version/基础信息整合.py rename to flask_app/old_version/基础信息整合_old.py diff --git a/flask_app/old_version/多线程分类.py b/flask_app/old_version/多线程分类_old.py similarity index 100% rename from flask_app/old_version/多线程分类.py rename to flask_app/old_version/多线程分类_old.py diff --git a/flask_app/old_version/废标项.py b/flask_app/old_version/废标项_old.py similarity index 100% rename from flask_app/old_version/废标项.py rename to flask_app/old_version/废标项_old.py diff --git a/flask_app/old_version/招标文件解析.py b/flask_app/old_version/招标文件解析_old.py similarity index 98% rename from flask_app/old_version/招标文件解析.py rename to flask_app/old_version/招标文件解析_old.py index 8209c31..7bcf816 100644 --- a/flask_app/old_version/招标文件解析.py +++ b/flask_app/old_version/招标文件解析_old.py @@ -5,14 +5,14 @@ import time from concurrent.futures import ThreadPoolExecutor from flask_app.main.截取pdf import truncate_pdf_multiple from flask_app.general.table_content_extraction import extract_tables_main -from flask_app.old_version.文档理解大模型版知识库处理.知识库操作 import addfileToKnowledge, deleteKnowledge +from flask_app.old_version.文档理解大模型版知识库处理.知识库操作_old import addfileToKnowledge, deleteKnowledge from flask_app.main.提取json工程标版 import convert_clause_to_json from flask_app.general.json_utils import transform_json_values from flask_app.main.无效标和废标和禁止投标整合 import combine_find_invalid from flask_app.main.投标人须知正文提取指定内容工程标 import extract_from_notice import concurrent.futures -from flask_app.old_version.基础信息整合 import combine_basic_info -from flask_app.old_version.资格审查模块old import combine_review_standards +from flask_app.old_version.基础信息整合_old import combine_basic_info +from flask_app.old_version.资格审查模块old_old import combine_review_standards from flask_app.main.商务评分技术评分整合 import combine_evaluation_standards from flask_app.general.format_change import pdf2docx, docx2pdf from flask_app.general.docx截取docx import copy_docx diff --git a/flask_app/old_version/文件分类普通版.py b/flask_app/old_version/文件分类普通版_old.py similarity index 100% rename from flask_app/old_version/文件分类普通版.py rename to flask_app/old_version/文件分类普通版_old.py diff --git a/flask_app/old_version/文档理解大模型版知识库处理/删除知识库.py b/flask_app/old_version/文档理解大模型版知识库处理/删除知识库_old.py similarity index 100% rename from flask_app/old_version/文档理解大模型版知识库处理/删除知识库.py rename to flask_app/old_version/文档理解大模型版知识库处理/删除知识库_old.py diff --git a/flask_app/old_version/文档理解大模型版知识库处理/知识库操作.py b/flask_app/old_version/文档理解大模型版知识库处理/知识库操作_old.py similarity index 97% rename from flask_app/old_version/文档理解大模型版知识库处理/知识库操作.py rename to flask_app/old_version/文档理解大模型版知识库处理/知识库操作_old.py index 19ee2ac..f0c1b2d 100644 --- a/flask_app/old_version/文档理解大模型版知识库处理/知识库操作.py +++ b/flask_app/old_version/文档理解大模型版知识库处理/知识库操作_old.py @@ -3,7 +3,7 @@ import uuid from llama_index.readers.dashscope.base import DashScopeParse from llama_index.readers.dashscope.utils import ResultType from llama_index.indices.managed.dashscope import DashScopeCloudIndex -from flask_app.old_version.文档理解大模型版知识库处理.删除知识库 import delete_index, create_client +from flask_app.old_version.文档理解大模型版知识库处理.删除知识库_old import delete_index, create_client def addfileToKnowledge(filepath,knowledge_name): diff --git a/flask_app/old_version/文档理解大模型版知识库处理/调用文件解析状态查询.py b/flask_app/old_version/文档理解大模型版知识库处理/调用文件解析状态查询_old.py similarity index 100% rename from flask_app/old_version/文档理解大模型版知识库处理/调用文件解析状态查询.py rename to flask_app/old_version/文档理解大模型版知识库处理/调用文件解析状态查询_old.py diff --git a/flask_app/old_version/文档理解大模型版知识库处理/调用文档解析异步提交.py b/flask_app/old_version/文档理解大模型版知识库处理/调用文档解析异步提交_old.py similarity index 100% rename from flask_app/old_version/文档理解大模型版知识库处理/调用文档解析异步提交.py rename to flask_app/old_version/文档理解大模型版知识库处理/调用文档解析异步提交_old.py diff --git a/flask_app/old_version/文档理解大模型版知识库处理/调用文档解析结果获取.py b/flask_app/old_version/文档理解大模型版知识库处理/调用文档解析结果获取_old.py similarity index 100% rename from flask_app/old_version/文档理解大模型版知识库处理/调用文档解析结果获取.py rename to flask_app/old_version/文档理解大模型版知识库处理/调用文档解析结果获取_old.py diff --git a/flask_app/old_version/无效标和废标和禁止投标整合.py b/flask_app/old_version/无效标和废标和禁止投标整合_old.py similarity index 99% rename from flask_app/old_version/无效标和废标和禁止投标整合.py rename to flask_app/old_version/无效标和废标和禁止投标整合_old.py index 20fdb94..67bf587 100644 --- a/flask_app/old_version/无效标和废标和禁止投标整合.py +++ b/flask_app/old_version/无效标和废标和禁止投标整合_old.py @@ -9,7 +9,7 @@ from flask_app.general.通义千问long import upload_file, qianwen_long from concurrent.futures import ThreadPoolExecutor from flask_app.general.table_content_extraction import extract_tables_main -from flask_app.old_version.不得存在及禁止投标情形 import find_forbidden, process_string_list +from flask_app.old_version.不得存在及禁止投标情形_old import find_forbidden, process_string_list #处理跨页的段落 diff --git a/flask_app/old_version/解析old.py b/flask_app/old_version/解析old_old.py similarity index 100% rename from flask_app/old_version/解析old.py rename to flask_app/old_version/解析old_old.py diff --git a/flask_app/old_version/资格审查模块old.py b/flask_app/old_version/资格审查模块old_old.py similarity index 97% rename from flask_app/old_version/资格审查模块old.py rename to flask_app/old_version/资格审查模块old_old.py index 4f640f2..6f93927 100644 --- a/flask_app/old_version/资格审查模块old.py +++ b/flask_app/old_version/资格审查模块old_old.py @@ -3,7 +3,7 @@ import os from flask_app.main.提取json工程标版 import convert_clause_to_json from flask_app.general.json_utils import extract_content_from_json from flask_app.old_version.形式响应评审old import process_reviews -from flask_app.old_version.资格评审old import process_qualification +from flask_app.old_version.资格评审old_old import process_qualification from flask_app.general.通义千问long import upload_file, qianwen_long from concurrent.futures import ThreadPoolExecutor diff --git a/flask_app/old_version/资格评审old.py b/flask_app/old_version/资格评审old_old.py similarity index 100% rename from flask_app/old_version/资格评审old.py rename to flask_app/old_version/资格评审old_old.py diff --git a/flask_app/old_version/资格评审前判断.py b/flask_app/old_version/资格评审前判断_old.py similarity index 100% rename from flask_app/old_version/资格评审前判断.py rename to flask_app/old_version/资格评审前判断_old.py diff --git a/flask_app/old_version/通义千问.py b/flask_app/old_version/通义千问_old.py similarity index 100% rename from flask_app/old_version/通义千问.py rename to flask_app/old_version/通义千问_old.py diff --git a/flask_app/start_up.py b/flask_app/start_up.py index d3c8077..9194eb3 100644 --- a/flask_app/start_up.py +++ b/flask_app/start_up.py @@ -9,7 +9,7 @@ from flask_app.routes.get_deviation import get_deviation_bp from flask_app.routes.little_zbparse import little_zbparse_bp from flask_app.routes.upload import upload_bp from flask_app.routes.test_zbparse import test_zbparse_bp - +from flask_app.general.清除file_id import delete_file_by_ids,read_file_ids class FlaskAppWithLimiter(Flask): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -23,25 +23,6 @@ def create_app(): handler.setFormatter(CSTFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) app.logger.addHandler(handler) app.logger.setLevel(logging.INFO) - # @app.before_request - # def before_request(): - # """ - # 每个请求开始前初始化 logger 和 output_folder, - # 根据请求的端点选择不同的子文件夹。 - # """ - # # 确定当前请求的端点 - # blueprint = request.blueprint - # # 映射端点到子文件夹 - # subfolder_map = { - # 'get_deviation': 'output3', - # 'little_zbparse': 'output2', - # 'upload': 'output1', - # 'test_zbparse': 'test_output' - # } - # # 获取对应的子文件夹,默认为 'output1' - # subfolder = subfolder_map.get(blueprint, 'output1') - # # 创建 logger 和 output_folder - # create_logger(app, subfolder) # 注册蓝图 app.register_blueprint(get_deviation_bp) @@ -58,6 +39,13 @@ def create_app(): monitor = getattr(g, 'monitor', None) if limiter and not monitor.is_timeout: limiter.semaphore.release() + output_folder = getattr(g, 'output_folder', None) + if output_folder: + # 执行与output_folder相关的清理操作(例如删除临时文件) + logger = g.logger + logger.info(f"正在清理输出文件夹: {output_folder}") + file_ids=read_file_ids(output_folder) + delete_file_by_ids(file_ids) return app #TODO:培训要求、总体要求、进度要求、'建设要求'到技术要求中,归类到其他要求中 diff --git a/flask_app/testdir/test3.py b/flask_app/testdir/test3.py index f5c4b4c..4ed3064 100644 --- a/flask_app/testdir/test3.py +++ b/flask_app/testdir/test3.py @@ -1,168 +1,3 @@ -import json -import re - - -def insert_missing_commas(json_str): - """ - 使用正则表达式在缺失逗号的位置插入逗号。 - 具体来说,寻找一个值的结束引号后紧跟着下一个键的开始引号,并在中间插入逗号。 - """ - # 这个正则匹配一个字符串结尾的引号,可能有空白字符,然后是另一个键的引号 - pattern = r'(":\s*"[^"]*)"\s*(")' - replacement = r'\1", \2' - - previous_str = None - while previous_str != json_str: - previous_str = json_str - json_str = re.sub(pattern, replacement, json_str) - - return json_str - - -def fix_json_escape_sequences(json_str): - """ - 修复 JSON 字符串中的非法转义序列。 - 将所有不符合 JSON 规范的反斜杠进行转义。 - """ - # JSON 中合法的转义字符 - valid_escapes = ['"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u'] - - # 使用正则表达式找到所有反斜杠 - # 如果反斜杠后面不是合法的转义字符,则进行转义 - pattern = re.compile(r'\\(?!["\\/bfnrtu])') - fixed_str = pattern.sub(r'\\\\', json_str) - return fixed_str - - -def replace_latex_expressions(json_str): - """ - 替换 JSON 字符串中的 LaTeX 风格表达式。 - 例如,将 $三 \geq 2 m$ 替换为 三 ≥2米 - """ - # 定义 LaTeX 符号到 Unicode 字符的映射 - latex_mapping = { - r'\geq': '≥', - r'\leq': '≤', - r'\times': '×', - r'\frac': '/', # 简单处理分数 - r'\neq': '≠', - r'\approx': '≈', - r'\pm': '±', - r'\alpha': 'α', - r'\beta': 'β', - r'\gamma': 'γ', - r'\delta': 'δ', - r'\pi': 'π', - r'\sqrt': '√', - r'\infty': '∞', - r'\cup': '∪', - r'\cap': '∩', - r'\subseteq': '⊆', - r'\supseteq': '⊇', - r'\forall': '∀', - r'\exists': '∃', - r'\rightarrow': '→', - r'\leftarrow': '←', - # 添加更多需要的映射 - } - - # 处理每一个 LaTeX 表达式 - def replace_match(match): - expr = match.group(1) - for latex, char in latex_mapping.items(): - expr = expr.replace(latex, char) - # 替换单位符号,例如 ' m' 到 '米', ' s' 到 '秒', 等等 - expr = re.sub(r'(\d+)\s*m', r'\1米', expr) - expr = re.sub(r'(\d+)\s*s', r'\1秒', expr) - expr = re.sub(r'(\d+)\s*kg', r'\1公斤', expr) - expr = re.sub(r'(\d+)\s*A', r'\1安', expr) # 例如电流单位安培 - # 继续添加更多单位 - return expr - - # 替换所有 $...$ 包围的内容 - fixed_str = re.sub(r'\$(.*?)\$', replace_match, json_str) - return fixed_str - - -def extract_content_from_json(string): - """ - 输入字符串,尝试解析 JSON 数据: - 1. 尝试直接解析原始 JSON。 - 2. 如果直接解析失败,按顺序使用不同修复方法尝试解析。 - """ - if not string.strip(): - return {} - - # 提取第一个匹配的 JSON 对象 - match = re.search(r'\{[\s\S]*\}', string) - if not match: - print("未找到有效的 JSON 内容。") - return {} - - original_json = match.group(0) - - # 尝试直接解析原始 JSON 数据 - try: - parsed = json.loads(original_json) - print("直接解析原始 JSON 成功。") - return parsed - except json.JSONDecodeError as original_error: - print(f"直接解析原始 JSON 失败: {original_error}") - - # 方法1:逗号修复 - try: - fixed_json1 = insert_missing_commas(original_json) - parsed = json.loads(fixed_json1) - print("使用方法1:逗号修复成功。") - return parsed - except json.JSONDecodeError as error1: - print(f"方法1(逗号修复)解析失败: {error1}") - - # 方法2:LaTeX 表达式替换 - try: - fixed_json2 = replace_latex_expressions(original_json) - parsed = json.loads(fixed_json2) - print("使用方法2:LaTeX 表达式替换成功。") - return parsed - except json.JSONDecodeError as error2: - print(f"方法2(LaTeX 替换)解析失败: {error2}") - - # 方法3:非法转义序列修复 - try: - fixed_json3 = fix_json_escape_sequences(original_json) - parsed = json.loads(fixed_json3) - print("使用方法3:非法转义序列修复成功。") - return parsed - except json.JSONDecodeError as error3: - print(f"方法3(非法转义修复)解析失败: {error3}") - - # 如果所有方法都失败,返回空字典 - print("所有修复方法均失败,返回空字典。") - return {} - - -def replace_latex_expressions_in_dict(obj): - """ - 递归遍历字典或列表,替换其中的 LaTeX 表达式。 - """ - if isinstance(obj, dict): - return {k: replace_latex_expressions_in_dict(v) for k, v in obj.items()} - elif isinstance(obj, list): - return [replace_latex_expressions_in_dict(item) for item in obj] - elif isinstance(obj, str): - # 仅处理被 $...$ 包围的内容 - def replace_match(match): - expr = match.group(1) - return replace_latex_expressions(expr) - - # 替换所有 $...$ 包围的内容 - return re.sub(r'\$(.*?)\$', replace_match, obj) - else: - return obj -test_json_1 = """ - { - "示例": "速度为 $v = 3 \\times 10^2 m/s$,加速度为 $a \\geq 9.8 m/s^2$。" - } - """ -res=extract_content_from_json(test_json_1) -print(res) \ No newline at end of file +data="" +if "哈哈" in data: + print("yes") \ No newline at end of file diff --git a/flask_app/货物标/商务服务其他要求提取.py b/flask_app/货物标/商务服务其他要求提取.py index a557dd7..f0a90c4 100644 --- a/flask_app/货物标/商务服务其他要求提取.py +++ b/flask_app/货物标/商务服务其他要求提取.py @@ -2,18 +2,19 @@ import json import re from PyPDF2 import PdfReader - +import textwrap from flask_app.general.doubao import read_txt_to_string, pdf2txt -from flask_app.general.json_utils import combine_json_results,clean_json_string -from flask_app.general.通义千问long import upload_file,qianwen_long_stream +from flask_app.general.json_utils import combine_json_results, clean_json_string +from flask_app.general.通义千问long import upload_file, qianwen_long_stream from flask_app.货物标.截取pdf货物标版 import extract_common_header, clean_page_content +import concurrent.futures from flask_app.general.doubao import doubao_model -#正则表达式判断原文中是否有商务、服务、其他要求 -def find_exists(truncate_file, required_keys): - if not truncate_file: - return ["技术要求", "商务要求", "服务要求", "其他要求"] +# 正则表达式判断原文中是否有商务、服务、其他要求 +def find_exists(truncate_file, required_keys): + # if not truncate_file: + # return ["技术要求", "商务要求", "服务要求", "其他要求"] common_header = extract_common_header(truncate_file) # 假设该函数已定义 pdf_document = PdfReader(truncate_file) @@ -33,63 +34,92 @@ def find_exists(truncate_file, required_keys): end_pattern = re.compile( r'第[一二三四五六七八九1-9]+(?:章|部分)\s*[\u4e00-\u9fff、()()]+\s*$', re.MULTILINE) - # 遍历所有页面,拼接全文 - text = "" - for page in pdf_document.pages: - page_text = page.extract_text() or "" - cleaned_text = clean_page_content(page_text, common_header) - text += cleaned_text + "\n" + # 只处理第一页和最后一页 + first_page = pdf_document.pages[0].extract_text() or "" + last_page = pdf_document.pages[-1].extract_text() or "" - # 匹配起始位置 - start_match = re.search(begin_pattern, text) + # 清理页面内容 + first_page_clean = clean_page_content(first_page, common_header) + last_page_clean = clean_page_content(last_page, common_header) + + # 在第一页寻找起始位置 + start_match = re.search(begin_pattern, first_page_clean) if not start_match: - print("未找到开始模式") - return [] - - start_index = start_match.end() - - # 匹配结束位置 - end_match = re.search(end_pattern, text[start_index:]) - if end_match: - end_index = start_index + end_match.start() - relevant_text = text[start_index:end_index] + print("未找到开始模式,返回完整第一页") + first_content = first_page_clean else: - relevant_text = text[start_index:] + start_index = start_match.end() + first_content = first_page_clean[start_index:] - # 保留换行,避免结构丢失 + # 在最后一页寻找结束位置 + end_match = re.search(end_pattern, last_page_clean) + if not end_match: + print("未找到结束模式,返回完整最后一页") + last_content = last_page_clean + else: + last_content = last_page_clean[:end_match.start()] + + # 获取中间页面的内容 + middle_content = "" + if len(pdf_document.pages) > 2: + for page_num in range(1, len(pdf_document.pages) - 1): + page_text = pdf_document.pages[page_num].extract_text() or "" + cleaned_text = clean_page_content(page_text, common_header) + middle_content += cleaned_text + "\n" + + # 组合所有内容 + relevant_text = first_content + "\n" + middle_content + "\n" + last_content relevant_text = re.sub(r'\s+', ' ', relevant_text) + # print(f"提取的内容范围:\n{relevant_text}") # 匹配所需的要求 matched_requirements = [] punctuation = r"[,。?!、;:,.?!]*" for req in required_keys: - # required_keys 中的元素本身已包含 \s*,直接作为正则模式 if re.search(req, relevant_text): - if req == "服\s*务\s*要\s*求": - # 提取所有包含"服务要求"的行 + # 替换特定的关键词 + if req in [r"总\s*体\s*要\s*求", r"建\s*设\s*要\s*求"]: + # 匹配到“总体要求”或“建设要求”时,添加“技术要求”而非原关键词 + tech_req = r"技\s*术\s*要\s*求" + if tech_req not in matched_requirements: + matched_requirements.append(tech_req) + elif req in [r"培\s*训\s*要\s*求",r"质\s*保\s*要\s*求",r"售\s*后\s*要\s*求"]: + # 匹配到“培训要求”时,添加“服务要求”而非原关键词 + service_req = r"服\s*务\s*要\s*求" + if service_req not in matched_requirements: + matched_requirements.append(service_req) + elif req in [r"进\s*度\s*要\s*求",r"工\s*期\s*要\s*求"]: + busi_req = r"商\s*务\s*要\s*求" + if busi_req not in matched_requirements: + matched_requirements.append(busi_req) + elif req == r"服\s*务\s*要\s*求": + # 处理“服务要求”时的特殊逻辑 lines = [line for line in relevant_text.split('\n') if re.search(req, line)] - # 检查是否存在'技术'紧跟在'服务要求'前面(中间只有标点,标点是可选的) - pattern = "技\s*术" + punctuation + req + pattern = r"技\s*术" + punctuation + req if any(re.search(pattern, line) for line in lines): - # 如果存在'技术'紧跟'服务要求',添加"技术、服务要求" - if "技\s*术\s*、\s*服\s*务\s*要\s*求" not in matched_requirements: - matched_requirements.append("技\s*术\s*、\s*服\s*务\s*要\s*求") + combined_req = r"技\s*术\s*、\s*服\s*务\s*要\s*求" + if combined_req not in matched_requirements: + matched_requirements.append(combined_req) else: - # 如果不存在'技术'紧跟'服务要求',正常添加"服务要求" - matched_requirements.append(req) + if req not in matched_requirements: + matched_requirements.append(req) else: - matched_requirements.append(req) - + # 对于其他匹配的关键词,直接添加 + if req not in matched_requirements: + matched_requirements.append(req) # 去除 \s*,仅返回原始关键词 clean_requirements = [re.sub(r'\\s\*', '', req) for req in matched_requirements] # 判断互斥关系:如果有"技术、服务要求",删除"技术要求"和"服务要求" if "技术、服务要求" in clean_requirements: clean_requirements = [req for req in clean_requirements if req not in ["技术要求", "服务要求"]] - + # 确保最终返回的列表仅包含指定的五项 + # allowed_requirements = {"技术要求", "服务要求", "商务要求", "其他要求", "技术、服务要求"} + # final_requirements = [req for req in clean_requirements if req in allowed_requirements] return clean_requirements + def generate_queries(truncate_file, required_keys): key_list = find_exists(truncate_file, required_keys) queries = [] @@ -104,48 +134,47 @@ def generate_queries(truncate_file, required_keys): # print(query_base) return queries -def generate_user_query_template(required_keys,processed_filepath): - import textwrap - import json - - # 定义所有可能的键 - all_possible_keys = ["技术要求", "服务要求", "商务要求", "其他要求", "技术、服务要求","总体要求","进度要求","培训要求"] +def generate_template(required_keys, type=1): # 定义每个键对应的示例内容 example_content1 = { "技术要求": ["相关技术要求1", "相关技术要求2"], - "服务要求": ["服务要求1", "服务要求2"], + "服务要求": ["服务要求1", "服务要求2", "服务要求3"], + "商务要求": ["商务要求1", "商务要求2"], + "其他要求": ["关于项目采购的其他要求1...", "关于项目采购的其他要求2...", "关于项目采购的其他要求3..."], + "技术、服务要求": ["相关技术、服务要求内容1", "相关技术、服务要求内容2", "相关技术、服务要求内容3"] + } + example_content2 = { + "技术要求": { + "子因素名1": ["相关技术要求1", "相关技术要求2"] + }, + "服务要求": { + "子因素名1": ["服务要求1"], + "子因素名2": ["服务要求2", "服务要求3"] + }, "商务要求": { "子因素名1": ["商务要求1"], "子因素名2": ["商务要求2"] }, "其他要求": { "子因素名1": ["关于项目采购的其他要求1...", "关于项目采购的其他要求2..."], - "子因素名2": ["关于项目采购的其他要求3...", "关于项目采购的其他要求4..."] + "子因素名2": ["关于项目采购的其他要求3..."] }, - "技术、服务要求": ["相关技术、服务要求内容1", "相关技术、服务要求内容2"] - } - - example_content2 = { - "技术要求": { - "子因素名1": ["相关技术要求1", "相关技术要求2"], - "子因素名2": ["相关技术要求3"] - }, - "服务要求": { - "子因素名1": ["相关服务要求1", "相关服务要求2"], - "子因素名2": ["相关服务要求3", "相关服务要求4"] - }, - "商务要求": ["商务要求1", "商务要求2"], - "其他要求": ["关于项目采购的其他要求1..."], "技术、服务要求": { "子因素名1": ["相关技术、服务要求内容1"], "子因素名2": ["相关技术、服务要求内容2", "相关技术、服务要求内容3"] } } - # 将 required_keys 转换为集合以便于操作 keys = set(required_keys) - + type_to_keys_map = { + 1: ["服务要求", "商务要求", "其他要求"], + 2: ["技术要求", "技术、服务要求"] + } + # 根据 type 获取对应的 all_possible_keys + chosen_keys = type_to_keys_map.get(type, []) + another_keys_list = type_to_keys_map.get(3 - type, []) # 3 - type 将 type 1 映射到 2,反之亦然 + another_keys_str = ', '.join([f"'{key}'" for key in another_keys_list]) # 处理互斥关系:如果 "技术要求" 和 "服务要求" 同时存在,则移除 "技术、服务要求" if "技术要求" in keys and "服务要求" in keys: keys.discard("技术、服务要求") @@ -155,121 +184,106 @@ def generate_user_query_template(required_keys,processed_filepath): keys.discard("服务要求") # 确保 keys 中只包含允许的键 - keys = keys.intersection(all_possible_keys) - + keys = keys.intersection(chosen_keys) # 按照预定义的顺序排序键,以保持一致性 - sorted_keys = [key for key in all_possible_keys if key in keys] + sorted_keys = [key for key in chosen_keys if key in keys] - # 如果没有任何键被选中,返回一个默认的模板或抛出异常 + # 如果没有任何键被选中,返回"" if not sorted_keys: - raise ValueError("required_keys 中没有有效的键。") + return "" - # 生成提示部分,根据 sorted_keys 动态构建 - keys_str = '、'.join(sorted_keys) - outer_keys_str = ', '.join([f"'{key}'" for key in sorted_keys]) - - # 使用三引号定义多行字符串,便于编辑和维护 - prompt_instruction = textwrap.dedent(f"""请你根据该货物类招标文件中的采购要求部分内容(技术、服务及商务要求部分内容),请告诉我该项目采购的{keys_str}分别是什么,请以json格式返回结果,默认情况下外层键名是{outer_keys_str},键值为字符串列表,每个字符串表示具体的一条要求,请按原文内容回答,保留三角▲、五角星★和序号(若有),不要擅自增添内容。 + # 生成模板的通用部分 + def generate_prompt_instruction(keys_str, outer_keys_str, another_keys_str, type): + if type == 1: + specific_instructions = textwrap.dedent( + """4. 若章节开头位置或者采购清单中除了需要采购的货物、数量、单位之外,还有带三角▲或五角星★的描述内容(如工期要求、进度要求、品牌要求等商务要求),也请将该部分内容提取出来,添加在外层键名为'商务要求'的键值部分;若存在标题包含'工期要求'、'进度要求'等和商务要求有关的关键字,也请将该标题下的内容提取,添加在外层键名为'商务要求'的键值部分。请不要遗漏这部分的'商务要求'。 + 5. 在提取'服务要求'的时候,若原文(包含正文和表格)中存在'安装要求'、'售后要求'、'维护要求'、'培训要求'等服务相关的要求说明,请添加至'服务要求'的键值部分,不要遗漏这部分的'服务要求'。 + """ + ) + else: + specific_instructions = textwrap.dedent( + """4. 在提取技术要求或技术、服务要求时,你无需从采购清单或表格中提取技术要求以及参数要求,你仅需定位到原文中包含'技术要求'或'技术、服务要求'关键字的标题并提取其后相关内容,从正文中提取;若技术要求或技术服务要求的内容全在表格中,键值为空列表[]。 + 5. 在提取'技术要求'时,注意不要提取有关'安装、售后、维护、运维、培训、质保'等要求,它们不属于'技术要求',但是若大标题中包含'总体要求''建设要求'等和技术要求相关的关键字,请添加到'技术要求'的键值部分。 + """ + ) + return textwrap.dedent( + f"""请你根据该货物类招标文件中的采购要求部分内容,请告诉我该项目采购的{keys_str}分别是什么,请以json格式返回结果,默认情况下外层键名是{outer_keys_str},键值为字符串列表,每个字符串表示具体的一条要求,可以按原文中的序号作划分(若有序号的话),请按原文内容回答,保留三角▲、五角星★和序号(若有),不要擅自增添内容及序号。注意:1. 若相应要求下存在子标题表示子要求因素,可以将它忽略而不是将它与下文具体要求进行多行合并,或者作为该要求下的嵌套键名,总之字符串列表中只提取具体的要求。2. 请不要提取{another_keys_str}中的内容。 要求与指南: - 1. 默认情况无需嵌套,键值为字符串列表;若存在嵌套结构,嵌套键名是原文中该要求下相应子标题,最多一层嵌套。 - 2. JSON 的结构要求: - - 外层键名为 {outer_keys_str} 中的各项。 + 1. JSON 的结构要求: + - 默认情况无需嵌套键值对,键值为字符串列表;若存在嵌套结构(即有明确标题表示各子要求),则嵌套键名是各子要求,嵌套键值为字符串列表,为该子要求下的若干要求,最多一层嵌套。 - 每个外层键对应的值可以是: - a. 一个对象(字典),其键为子因素名,值为字符串列表。 - b. 一个字符串列表,表示具体的一条条要求。若只有一条要求,也用字符串列表表示。 + a. 一个字符串列表,表示具体的一条条要求。若只有一条要求,也用字符串列表表示。 + b. 一个对象(字典),其键为子因素名,值为字符串列表。 + c. 若文档中无符合的相关要求,键值为空列表[] - 最多只允许一层嵌套。 - 3. 请优先定位正文部分的大标题'xx要求',在其之后提取'xx要求'相关内容, - 4. 若章节开头位置或者采购清单中除了需要采购的货物、数量、单位之外,还有带三角▲或五角星★的描述内容(如工期要求、质保要求等商务要求),请将该部分内容提取出来,添加在键名为'商务要求'的字典的键值部分,注意请不要返回Markdown语法,必要时使用冒号':'将相关信息拼接在一起。 - 5. 在提取技术要求或技术、服务要求时(若有),你无需从采购清单或表格中提取货物名以及参数要求,你仅需定位到原文中大标题'技术要求'或'技术、服务要求'部分提取正文内容,若内容全在表格中,键值为空列表[]。 - 6. 若无相关要求,键值为[] - """ ) + 2. 请优先且准确定位正文部分包含以下关键字的标题:{outer_keys_str},在其之后提取'XX要求'相关内容,尽量避免在无关地方提取内容。 + 3. 注意请不要返回Markdown表格语法,必要时使用冒号':'将相关信息拼接在一起 + {specific_instructions} + 6. 字符串列表中的每个字符串内容需与原文内容保持一致,保留前面的三角▲、五角星★和序号(若有),而且你不可以擅自添加序号。 + """) - # 过滤 example_content1 和 example_content2 以仅包含 sorted_keys - def filter_content(example_content, keys): + # 过滤示例内容 + def filter_example_content(example_content, keys): return {k: v for k, v in example_content.items() if k in keys} - filtered_example_content1 = filter_content(example_content1, sorted_keys) - filtered_example_content2 = filter_content(example_content2, sorted_keys) + def format_example(example_content): + return json.dumps(example_content, indent=4, ensure_ascii=False) - # 将过滤后的示例转换为格式化的 JSON 字符串 - json_example1_str = json.dumps(filtered_example_content1, indent=4, ensure_ascii=False) - json_example2_str = json.dumps(filtered_example_content2, indent=4, ensure_ascii=False) - # 从文件中读取内容 - # full_text = read_txt_to_string(processed_filepath) + filtered_example_content1 = filter_example_content(example_content1, sorted_keys) + filtered_example_content2 = filter_example_content(example_content2, sorted_keys) + tech_json_example1_str = format_example(filtered_example_content1) + tech_json_example2_str = format_example(filtered_example_content2) + keys_str = '、'.join(sorted_keys) + outer_keys_str = ', '.join([f"'{key}'" for key in sorted_keys]) + prompt_instruction = generate_prompt_instruction(keys_str, outer_keys_str, another_keys_str, type) # 完整的用户查询模板,包含两份示例输出 user_query_template = f""" -{prompt_instruction} -以下为示例输出,仅供格式参考: -示例 1: -{json_example1_str} -示例 2: -{json_example2_str} - -""" - # 文本内容:{full_text} + {prompt_instruction} + 以下为示例输出,仅供格式参考: + 示例 1,无嵌套键值对: + {tech_json_example1_str} + 示例 2,嵌套键值对形式: + {tech_json_example2_str} + """ return user_query_template -def merge_requirements(input_dict): - # 初始化一个临时字典,用于存储标准化后的键 - temp_dict = {} - # 初始化最终字典,只包含指定的四个键 - final_keys = ['技术要求', '商务要求', '服务要求', '其他要求'] - final_dict = {key: "" for key in final_keys} - - # 如果输入字典中有'其他要求',保留其内容 - if '其他要求' in temp_dict and temp_dict['其他要求'].strip(): - final_dict['其他要求'] = temp_dict['其他要求'].strip() - - # 处理'技术要求', '商务要求', '服务要求' - for key in ['技术要求', '商务要求', '服务要求']: - if key in temp_dict: - final_dict[key] = temp_dict[key].strip() - - # 收集需要合并到'其他要求'的内容 - merge_keys = ['总体要求', '进度要求', '培训要求'] - merged_contents = [] - for key in merge_keys: - if key in temp_dict and temp_dict[key].strip(): - merged_contents.append(temp_dict[key].strip()) - - # 如果有需要合并的内容 - if merged_contents: - merged_text = " ".join(merged_contents) - if final_dict['其他要求']: - final_dict['其他要求'] += " " + merged_text - else: - final_dict['其他要求'] = merged_text - - # 移除多余的空格 - for key in final_dict: - final_dict[key] = final_dict[key].strip() - - return final_dict -#,"总\s*体\s*要\s*求","进\s*度\s*要\s*求","培\s*训\s*要\s*求" -def get_business_requirements(procurement_path,processed_filepath): - file_id=upload_file(procurement_path) - required_keys = ["技\s*术\s*要\s*求","商\s*务\s*要\s*求", "服\s*务\s*要\s*求", "其\s*他\s*要\s*求"] - contained_keys=find_exists(procurement_path,required_keys) +def get_business_requirements(procurement_path,procurement_docpath): + file_id = upload_file(procurement_docpath) + print(file_id) + required_keys = ["技\s*术\s*要\s*求", "商\s*务\s*要\s*求", "服\s*务\s*要\s*求", "其\s*他\s*要\s*求","总\s*体\s*要\s*求","建\s*设\s*要\s*求","进\s*度\s*要\s*求","工\s*期\s*要\s*求","质\s*保\s*要\s*求","培\s*训\s*要\s*求","售\s*后\s*要\s*求"] + contained_keys = find_exists(procurement_path, required_keys) print(contained_keys) if not contained_keys: return {} # queries = generate_queries(truncate_file, contained_keys) - user_query=generate_user_query_template(contained_keys,processed_filepath) - # print(user_query) - model_res=qianwen_long_stream(file_id,user_query) - # model_res=doubao_model(user_query) - # Combine and fill missing keys with default values - final_res = clean_json_string(model_res) - # final_res.update({key: final_res.get(key, "") for key in required_keys}) + busi_user_query = generate_template(contained_keys, 1) + tech_user_query = generate_template(contained_keys, 2) + final_res={} + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + futures = [] + if busi_user_query: + futures.append(executor.submit(qianwen_long_stream, file_id, busi_user_query, 2, 1)) + if tech_user_query: + futures.append(executor.submit(qianwen_long_stream, file_id, tech_user_query, 2, 1)) + # 获取结果 + for future in concurrent.futures.as_completed(futures): + try: + result = future.result() + if result: # 确保结果不为空 + final_res.update(clean_json_string(result)) + except Exception as e: + print(f"An error occurred: {e}") return final_res -#TODO:改为先判断,再摘取 + +# TODO:改为先判断,再摘取 if __name__ == "__main__": # truncate_file = "C:\\Users\\Administrator\\Desktop\\fsdownload\\e4be098d-b378-4126-9c32-a742b237b3b1\\ztbfile_procurement.docx" - truncate_file=r"C:\Users\Administrator\Desktop\fsdownload\fa0d51a1-0d63-4c0d-9002-cf8ac3f2211a\ztbfile_procurement.pdf" - + truncate_file = r"C:\Users\Administrator\Desktop\货物标\output1\陕西省公安厅交通警察总队高速公路交通安全智能感知巡查系统项目 (1)_procurement.pdf" + docx_path=r'C:\Users\Administrator\Desktop\货物标\output1\陕西省公安厅交通警察总队高速公路交通安全智能感知巡查系统项目 (1)_procurement.docx' + # truncate_file=r"C:\Users\Administrator\Desktop\new招标文件\output5\HBDL-2024-0519-001-招标文件_procurement.pdf" # file_id = upload_file(truncate_file) processed_filepath = pdf2txt(truncate_file) - res=get_business_requirements(truncate_file,processed_filepath) - print(json.dumps(res, ensure_ascii=False, indent=4)) + final_res= get_business_requirements(truncate_file, docx_path) + print(json.dumps(final_res, ensure_ascii=False, indent=4)) diff --git a/flask_app/货物标/技术参数要求提取.py b/flask_app/货物标/技术参数要求提取.py index ae29084..2f8bad5 100644 --- a/flask_app/货物标/技术参数要求提取.py +++ b/flask_app/货物标/技术参数要求提取.py @@ -290,151 +290,131 @@ def combine_and_update_results(original_data, updates): return original_data -# user_query1 = """ -# 请你首先定位该采购文件中的采购清单或采购需求部分,请告诉我需要采购的货物,如果有采购清单,请直接根据清单上的货物(或系统)名称给出结果,注意不要返回'说明'或'规格'或'技术参数'列中的内容;若没有采购清单,你要从表格中或文中摘取需要采购的系统和货物,采购需求中可能包含层次关系,例如采购的某系统中可能包含几种货物,那么你需要用嵌套键值对表示这种关系,且不要遗漏该系统中包含的货物,你的输出请以json格式返回,最外层键名为'采购需求',嵌套键名为对应的系统名称或货物名称,需与原文保持一致,无需给出采购数量和单位。以下为需要考虑的特殊情况:如果采购清单中同一层级(或同一系统)下存在同名货物且它们的采购要求有所不同,请你以'货物名-编号'区分多种型号,编号为从 1 开始的自然数,依次递增,例如若采购清单中有两种型号的'交换机',那么你应返回两个键名,'交换机-1'和'交换机-2';如有未知内容,在对应键值处填'未知'。以下为考虑了特殊情况的示例输出: -# { -# "采购需求": { -# "交换机-1":{}, -# "交换机-2":{}, -# "门禁管理系统": {}, -# "交通监控视频子系统": { -# "高清视频抓拍像机":{}, -# "补光灯":{} -# }, -# "LED全彩显示屏": {} -# } -# } -# """ - #文件内容以markdown格式组织,其中表格部分(若有)以html语法组织, def get_technical_requirements(file_path,invalid_path,processed_filepath): - docx_file_path=pdf2docx(file_path) - file_id=upload_file(docx_file_path) + # docx_file_path=pdf2docx(file_path) + file_id=upload_file(file_path) #目前传入的为docx文档 first_query_template="该文件是否说明了采购需求,即需要采购哪些货物?如果有,请回答'是',否则,回答'否'" #防止截取失败 judge_res=qianwen_long(file_id,first_query_template) prompt_template1 = ''' - 任务:解析采购文件,提取采购需求,并以JSON格式返回。 +任务:解析采购文件,提取采购需求,并以JSON格式返回。 - 要求与指南: - 1. 精准定位:请运用文档理解能力,找到文件中的采购需求部分,若有采购清单,请直接根据采购清单上的货物(或系统)名称给出结果。 - 2. 采购目标:采购目标通常有硬件(如设备、货物)和软件(如系统软件、应用APP),一次采购活动可能同时包含这两种类型。对于工程类的施工、建设采购需求,无需提取。 - 3. 非清单形式处理:若未出现采购清单,则从表格或文字中摘取采购信息。 - 4. 系统归属:一些采购活动可能将采购目标划分为若干系统和货物,每个系统可能包含若干货物,则将这些货物名称作为该系统的二级键;系统可以只包含总体'系统功能'而无货物。 - 5. 软件需求:对于软件应用或系统软件需求,仅需列出系统模块构成(若有),并作为系统键值的一部分,无需在模块下再细分功能。 - 6. 系统功能:若采购的某系统提及总体系统功能,则在系统值中添加'系统功能'二级键,不展开具体内容。 - 7. 完整性:确保不遗漏系统内的货物,也不添加未提及的内容。 +要求与指南: +1. 精准定位:请运用文档理解能力,找到文件中的采购需求部分,若有采购清单,请直接根据采购清单上的货物(或系统)名称给出结果;若未出现采购清单,则从表格或文字中摘取采购信息 +2. 采购目标:采购种类通常有硬件(如设备、货物)和软件(如系统软件、应用APP),一次采购活动可以同时包含这两种类型。 +3. 系统归属:一些采购活动可能将采购目标划分为若干系统和货物,每个系统可能包含若干货物,则将这些货物名称作为该系统的二级键;系统可以只包含总体'系统功能'而无货物。 +4. 软件需求:对于软件应用或系统软件需求,仅需列出系统模块构成(若有),并作为系统键值的一部分,无需在模块下再细分功能。 +5. 系统功能:若采购的某系统提及总体系统功能,则在系统值中添加'系统功能'二级键,不展开具体内容。 +6. 完整性:确保不遗漏系统内的货物,也不添加未提及的内容,若采购清单之外有额外的货物采购要求,且该货物暂未提取至JSON回答中,请将这些货物名称也包含进来。 - 输出格式: - 1.JSON格式,最外层键名为'采购需求'。 - 2.层次关系用嵌套键值对表示。 - 3.嵌套键名为系统或货物或模块名称,与原文保持一致。 - 4.最内层键值应为空列表[]。 - 5.不包含'说明'、'规格'、'技术参数'等列内容,仅返回采购的货物或系统或模块名称。 +输出格式: +1.JSON格式,最外层键名为'采购需求'。 +2.层次关系用嵌套键值对表示。 +3.嵌套键名为系统或货物或模块名称,与原文保持一致。 +4.最内层键值应为空列表[]。 +5.不包含'说明'、'规格'、'技术参数'等列内容,仅返回采购的货物或系统或模块名称。 - 特殊情况处理: - 同一层级(如同一系统中)下同名但采购要求不同的货物,以'货物名-编号'区分,编号从1递增。例如若同层级下存在两种型号的交换机,那么命名分别是'交换机-1'和'交换机-2',以规避重复键名;否则无需在名称后添加编号。 +特殊情况处理: +同一层级(如同一系统中)下同名但采购要求不同的货物,以'货物名-编号'区分,编号从1递增。例如若同层级下存在两种型号的交换机,那么命名分别是'交换机-1'和'交换机-2',以规避重复键名;否则无需在名称后添加编号。 - {{ - "采购需求": {{ - "交换机-1": [], - "交换机-2": [], - "门禁管理系统": {{ - "系统功能":[] - }}, - "交通监控视频子系统": {{ - "系统功能": [], - "高清视频抓拍像机": [], - "补光灯": [] - }}, - "LED全彩显示屏": [] - // 其他系统和货物 - }} - }} - 示例输出2,系统软件采购: - {{ - "采购需求": {{ - "信息管理系统": {{ - "通用模块":[], - "用户管理":[] - }}, - "信息检索系统": {{ - "系统功能":[], - "权限管理模块":[] - }}, - "XX小程序":[], - "数据分析中心":[] - }} - }} +{{ + "采购需求": {{ + "交换机-1": [], + "交换机-2": [], + "门禁管理系统": {{ + "系统功能":[] + }}, + "交通监控视频子系统": {{ + "系统功能": [], + "高清视频抓拍像机": [], + "补光灯": [] + }}, + "LED全彩显示屏": [] + // 其他系统和货物 + }} +}} +示例输出2,系统软件采购: +{{ + "采购需求": {{ + "信息管理系统": {{ + "通用模块":[], + "用户管理":[] + }}, + "信息检索系统": {{ + "系统功能":[], + "权限管理模块":[] + }}, + "XX小程序":[], + "数据分析中心":[] + }} +}} - 注意事项: - 1.严格按照上述要求执行,确保输出准确性和规范性。 - 2.如有任何疑问或不确定内容,请保留原文描述,必要时使用'未知'标注。 - ''' +注意事项: +1.严格按照上述要求执行,确保输出准确性和规范性。 +2.如有任何疑问或不确定内容,请保留原文描述,必要时使用'未知'标注。 +''' prompt_template2 = ''' - 任务:解析采购文件,提取采购需求,并以JSON格式返回。 +任务:你负责解析采购文件,提取采购需求,并以JSON格式返回,不要遗漏该项目需要采购的货物(或系统)。 - 要求与指南: - 1. 精准定位:请运用文档理解能力,找到文件中的采购需求部分,若有采购清单,请直接根据采购清单上的货物(或系统)名称给出结果。 - 2. 采购目标:采购目标通常有硬件(如设备、货物)和软件(如系统软件、应用APP),一次采购活动可能同时包含这两种类型。对于工程类的施工、建设采购需求,无需提取。 - 3. 非清单形式处理:若未出现采购清单,则从表格或文字中摘取采购信息。 - 4. 系统归属:一些采购活动可能将采购目标划分为若干系统和货物,每个系统可能包含若干货物,则将这些货物名称作为该系统的二级键;系统可以只包含总体'系统功能'而无货物。 - 5. 软件需求:对于软件应用或系统软件采购,若有多个系统且序号分明,请不要遗漏,最多仅需列出系统模块构成(若有),并作为该系统键值的一部分,无需在模块下再细分功能。 - 6. 系统功能:若采购的某系统提及总体系统功能,则在系统值中添加'系统功能'二级键,不展开具体内容。 - 7. 完整性:确保不遗漏系统内的货物,也不添加未提及的内容。 +要求与指南: +1. 精准定位:请运用文档理解能力,定位文件中的采购需求部分。若有采购清单,请直接根据采购清单上的货物(或系统)名称给出结果,注意你无需提取诸如'说明'、'规格'、'技术参数'、'描述'等列的内容,即你不需要给出详细的采购要求,仅返回采购的货物或系统或模块名称;若没有采购清单,则从表格或文本中摘取采购信息。 +2. 采购目标:采购种类通常有硬件(如设备、货物)和软件(如系统软件、应用APP),一次采购活动可以同时包含这两种类型。 +3. 系统归属:一些采购活动可能将采购目标划分为若干系统和货物,每个系统可能包含若干货物,则将这些货物名称作为该系统的二级键,注意这种包含关系是通过表格结构或文档标题层次来得出的;系统可以只包含总体'系统功能'而无货物。 +5. 软件需求:对于软件应用或系统软件采购,若有多个系统且序号分明,请不要遗漏,最多仅需列出系统模块构成(若有),并作为该系统键值的一部分,无需在模块下再细分功能。 +5. 系统功能:若采购的某系统提及总体系统功能,则在系统值中添加'系统功能'二级键,不展开具体内容。 +6. 完整性:确保不遗漏系统内的货物,也不添加未提及的内容。若'采购清单'中未提取的货物(或系统)名称在形如'主要设备功能指标'的标题下有详细参数指标要求,请将该货物名也添加至返回中。 - 输出格式: - 1.JSON格式,最外层键名为'采购需求'。 - 2.层次关系用嵌套键值对表示。 - 3.嵌套键名为系统或货物或模块名称,与原文保持一致。 - 4.最内层键值应为空列表[]。 - 5.不包含'说明'、'规格'、'技术参数'等列内容,仅返回采购的货物或系统或模块名称。 +输出格式: +1.JSON格式,最外层键名为'采购需求'。 +2.层次关系用嵌套键值对表示。 +3.嵌套键名为系统或货物或模块名称,与原文保持一致。 +4.最内层键值应为空列表[]。 - 特殊情况处理: - 若同一层级(如同一系统中)下存在同名但采购要求不同的货物,以'货物名-编号'区分,编号从1递增,例如若同层级下存在两种型号的交换机,那么命名分别是'交换机-1'和'交换机-2',以规避重复键名;否则无需在名称后添加编号。 +特殊情况处理: +若同一层级(如同一系统中)下存在同名但采购要求不同的货物,请以'货物名-编号'区分,编号从1递增,例如若同层级下存在两种型号的交换机,那么命名分别是'交换机-1'和'交换机-2',以规避重复键名;否则无需在名称后添加编号。 - 示例输出1,普通系统、货物类采购: - {{ - "采购需求": {{ - "交换机-1": [], - "交换机-2": [], - "门禁管理系统": {{ - "系统功能":[] - }}, - "交通监控视频子系统": {{ - "系统功能": [], - "交换机":[], - "高清视频抓拍像机": [], - "补光灯": [] - }}, - "LED全彩显示屏": [] - // 其他系统和货物 - }} - }} - 示例输出2,系统软件采购: - {{ - "采购需求": {{ - "信息管理系统": {{ - "通用模块":[], - "用户管理":[] - }}, - "信息检索系统": {{ - "系统功能":[], - "权限管理模块":[] - }}, - "XX管理系统":[], - "XX管理系统":[] - }} - }} +示例输出1,普通系统、货物类采购: +{{ + "采购需求": {{ + "交换机-1": [], + "交换机-2": [], + "门禁管理系统": {{ + "系统功能":[] + }}, + "交通监控视频子系统": {{ + "系统功能": [], + "交换机":[], + "高清视频抓拍像机": [], + "补光灯": [] + }}, + "LED全彩显示屏": [] + // 其他系统和货物 + }} +}} +示例输出2,软件系统类采购: +{{ + "采购需求": {{ + "信息管理系统": {{ + "通用模块":[], + "用户管理":[] + }}, + "信息检索系统": {{ + "系统功能":[], + "权限管理模块":[] + }}, + "XX管理系统":[], + //其他系统 + }} +}} - 文件内容:{full_text} +文件内容:{full_text} - 注意事项: - 1.严格按照上述要求执行,确保输出准确性和规范性。 - 2.如有任何疑问或不确定内容,请保留原文描述,必要时使用'未知'标注。 - ''' +注意事项: +1.严格按照上述要求执行,确保输出准确性和规范性。 +''' if '否' in judge_res: + print("no!调用invalid_path") file_id=upload_file(invalid_path) - print("调用invalid_path") model_res=qianwen_long(file_id,prompt_template1) print(model_res) else: @@ -444,7 +424,6 @@ def get_technical_requirements(file_path,invalid_path,processed_filepath): model_res=doubao_model(user_query) # model_res = qianwen_long(file_id,prompt_template1) print(model_res) - cleaned_res = clean_json_string(model_res) #转字典 processed_data=truncate_system_keys(cleaned_res['采购需求']) key_paths, grouped_paths, good_list, data_copy= generate_key_paths(processed_data) # 提取需要采购的货物清单 key_list:交通监控视频子系统.高清视频抓拍像机 ... grouped_paths是同一系统下同时有'交换机-1'和'交换机-2',提取'交换机' ,输出eg:{'交通标志.标志牌铝板', '交通信号灯.交换机'} @@ -571,16 +550,17 @@ def test_all_files_in_folder(input_folder, output_folder): if __name__ == "__main__": start_time=time.time() # truncate_file="C:\\Users\\Administrator\\Desktop\\fsdownload\\469d2aee-9024-4993-896e-2ac7322d41b7\\ztbfile_procurement.docx" - truncate_file=r"C:\Users\Administrator\Desktop\fsdownload\5950ad84-30c8-4643-b6de-b13ef5be7a5c\ztbfile.pdf" + truncate_docfile=r"C:\Users\Administrator\Desktop\货物标\output1\招标文件(107国道)_procurement.docx" + truncate_file=r'C:\Users\Administrator\Desktop\fsdownload\e702f1e6-095d-443d-bb7d-ef2e42037cb1\ztbfile_procurement.pdf' # invalid_path="D:\\flask_project\\flask_app\\static\\output\\output1\\e7dda5cb-10ba-47a8-b989-d2993d34bb89\\ztbfile.pdf" # truncate_file="D:\\flask_project\\flask_app\\static\\output\\output1\\e7dda5cb-10ba-47a8-b989-d2993d34bb89\\ztbfile_procurement.docx" # output_folder="C:\\Users\\Administrator\\Desktop\\货物标\\output1\\tmp" # file_id = upload_file(truncate_file) - invalid_path="C:\\Users\\Administrator\\Desktop\\fsdownload\\a110ed59-00e8-47ec-873a-bd4579a6e628\\ztbfile.pdf" + invalid_path=r"C:\Users\Administrator\Desktop\fsdownload\a110ed59-00e8-47ec-873a-bd4579a6e628\ztbfile.pdf" # file_id=upload_file(truncate_file) - # processed_filepath = pdf2txt(truncate_file) - processed_filepath=r"C:\Users\Administrator\Desktop\fsdownload\5950ad84-30c8-4643-b6de-b13ef5be7a5c\tmp\extract1.txt" - res=get_technical_requirements(truncate_file,invalid_path,processed_filepath) + processed_filepath = pdf2txt(truncate_file) + # processed_filepath=r"C:\Users\Administrator\Desktop\fsdownload\e702f1e6-095d-443d-bb7d-ef2e42037cb1\金水河沿线排涝泵站提档升级项目.txt" + res=get_technical_requirements(truncate_docfile,invalid_path,processed_filepath) json_string = json.dumps(res, ensure_ascii=False, indent=4) print(json_string) # # input_folder = "C:\\Users\\Administrator\\Desktop\\货物标\\output1" diff --git a/flask_app/货物标/提取采购需求main.py b/flask_app/货物标/提取采购需求main.py index 19566be..d4f05c6 100644 --- a/flask_app/货物标/提取采购需求main.py +++ b/flask_app/货物标/提取采购需求main.py @@ -4,14 +4,15 @@ import time from flask_app.general.doubao import pdf2txt from flask_app.general.file2markdown import convert_pdf_to_markdown +from flask_app.general.format_change import pdf2docx from flask_app.货物标.技术参数要求提取 import get_technical_requirements from flask_app.general.通义千问long import upload_file from flask_app.货物标.商务服务其他要求提取 import get_business_requirements -#获取采购清单 +# 获取采购清单 def fetch_procurement_reqs(procurement_path, invalid_path): - # procurement_docpath = pdf2docx(procurement_path) # 采购需求docx + procurement_docpath = pdf2docx(procurement_path) # 采购需求docx # 定义默认的 procurement_reqs 字典 DEFAULT_PROCUREMENT_REQS = { "采购需求": "", @@ -26,14 +27,16 @@ def fetch_procurement_reqs(procurement_path, invalid_path): return DEFAULT_PROCUREMENT_REQS.copy() try: - # processed_filepath = convert_pdf_to_markdown(procurement_path) # 转markdown格式 + # processe:d_filepath = convert_pdf_to_markdown(procurement_path) # 转markdown格式 processed_filepath = pdf2txt(procurement_path) # 纯文本提取 # 使用 ThreadPoolExecutor 并行处理 get_technical_requirements 和 get_business_requirements with concurrent.futures.ThreadPoolExecutor() as executor: + # 提交任务给线程池 - future_technical = executor.submit(get_technical_requirements, procurement_path, invalid_path,processed_filepath) + future_technical = executor.submit(get_technical_requirements, procurement_docpath, invalid_path, + processed_filepath) time.sleep(0.5) # 保持原有的延时 - future_business = executor.submit(get_business_requirements, procurement_path,processed_filepath) + future_business = executor.submit(get_business_requirements, procurement_path, procurement_docpath) # 获取并行任务的结果 technical_requirements = future_technical.result() @@ -44,8 +47,6 @@ def fetch_procurement_reqs(procurement_path, invalid_path): "采购需求": technical_requirements.get("采购需求", {}) } - # 合并 business_requirements 到 procurement_reqs 中 - # 这样无论 business_requirements 包含什么键(如 "技术要求"、"服务要求" 或 "技术、服务要求"),都将被保留 procurement_reqs.update(business_requirements) # 如果需要确保所有默认键存在,可以取消下面的注释 @@ -60,16 +61,17 @@ def fetch_procurement_reqs(procurement_path, invalid_path): return DEFAULT_PROCUREMENT_REQS.copy() -#TODO:技术要求可以在技术参数之后执行,把完整的技术参数输入,问大模型,除了上述内容还有哪些,这样的话把技术标和其他的区分开。 -#TODO: 094有问题 +# TODO:技术要求可以在技术参数之后执行,把完整的技术参数输入,问大模型,除了上述内容还有哪些,这样的话把技术标和其他的区分开。 +# TODO:0362 + if __name__ == "__main__": - start_time=time.time() + start_time = time.time() output_folder = "C:\\Users\\Administrator\\Desktop\\货物标\\货物标output" # file_path="C:\\Users\\Administrator\\Desktop\\货物标\\output1\\2-招标文件(2020年广水市中小学教师办公电脑系统及多媒体“班班通”设备采购安装项目)_procurement.pdf" - procurement_path = r"C:\Users\Administrator\Desktop\货物标\output1\招标文件(107国道)_procurement.pdf" - procurement_docpath=r"C:\Users\Administrator\Desktop\fsdownload\fa0d51a1-0d63-4c0d-9002-cf8ac3f2211a" - invalid_path="C:\\Users\\Administrator\\Desktop\\fsdownload\\db79e9e0-830e-442c-8cb6-1d036215f8ff\\ztbfile.pdf" - res=fetch_procurement_reqs(procurement_path,invalid_path) + procurement_path = r"C:\Users\Administrator\Desktop\货物标\output1\陕西省公安厅交通警察总队高速公路交通安全智能感知巡查系统项目 (1)_procurement.pdf" + procurement_docpath = r"C:\Users\Administrator\Desktop\fsdownload\fa0d51a1-0d63-4c0d-9002-cf8ac3f2211a" + invalid_path = "C:\\Users\\Administrator\\Desktop\\fsdownload\\db79e9e0-830e-442c-8cb6-1d036215f8ff\\ztbfile.pdf" + res = fetch_procurement_reqs(procurement_path, invalid_path) print(json.dumps(res, ensure_ascii=False, indent=4)) - end_time=time.time() - print("耗时:"+str(end_time-start_time)) + end_time = time.time() + print("耗时:" + str(end_time - start_time)) diff --git a/flask_app/货物标/评分标准提取main.py b/flask_app/货物标/评分标准提取main.py index c422e8d..b6aead4 100644 --- a/flask_app/货物标/评分标准提取main.py +++ b/flask_app/货物标/评分标准提取main.py @@ -225,7 +225,14 @@ def combine_evaluation_standards(truncate_file): user_query = ( """ 根据该文档中的评分标准表格中的内容,请你列出该文件的技术评分,商务评分,投标报价评审以及它们对应的具体评分要求,请以json格式返回结果,最外层键名分别是'技术评分','商务评分','投标报价评分',请在这三大项评分中分别用若干键值对表示具体评分项,外层键名为各评审因素,键值为一个列表,列表中包含若干(可为一)描述该评审因素的评分及要求的字典,内层键名分别是'评分'和'要求',若无评分,可删去'评分'键值对,'要求'中说明了该评审因素的评分标准;若这三大项评分中存在其他信息,则在相应评分大块内部新增键名'备注'存放该信息,键值为具体的要求,否则不需要。如果评分内容(因素)不是这三大项,则返回文档中给定的评分内容(因素)以及它们的具体评分要求。 -以下为需要考虑的注意事项:1.不要回答有关资格审查、符合性审查的内容,也不要从评标办法正文中(表格外)提取回答 2.若大项的'xx评分'要求未在文中说明,则键名'xx评分'的键值设为'本项目无xx评分项',例如"技术评分":"本项目无技术评分项" 3. 如果该招标活动有多个包,则最外层键名为对应的包名,否则不需要 4.你无需将表格的单元格内的内容进行拆分,需要将它视为一个整体。以下为示例输出,仅供格式参考: +要求与指南: +1.请首先定位评分细则的表格,不要回答有关资格审查、符合性审查的内容,也不要从评标办法正文中(表格外)提取回答 +2.若大项的'xx评分'要求未在文中说明,则键名'xx评分'的键值设为'本项目无xx评分项',例如"技术评分":"本项目无技术评分项" +3. 如果该招标活动有多个包,则最外层键名为对应的包名,否则不需要 +4.你无需将表格的单元格内的内容进行拆分,需要将它视为一个整体。 +5. '评分'的键值不能是一个范围数字,如'0-5分',应该是一个具体数字,如'5分',或者是一个定性的指标如'合格制' + +以下为示例输出,仅供格式参考: { "一包": { "技术评分": { diff --git a/flask_app/货物标/货物标解析main.py b/flask_app/货物标/货物标解析main.py index db69b4e..f519eee 100644 --- a/flask_app/货物标/货物标解析main.py +++ b/flask_app/货物标/货物标解析main.py @@ -213,6 +213,7 @@ def post_process_baseinfo(base_info,logger): logger.error(f"Error in post_process_baseinfo: {e}") return base_info, [] # 返回空列表 +#TODO:错误处理,通过返回值completion来错误处理,而不是正则表达 学习装饰器、 整体后处理 def goods_bid_main(output_folder, file_path, file_type, unique_id): logger = get_global_logger(unique_id) # 预处理文件,获取处理后的数据