项目文件夹

文件
wehub-resource-sync e768098d0e
tools_continuous_delivery / Private PyPI non-main branch release (push) Has been skipped
tools_continuous_delivery / Private PyPI main branch release (push) Failing after 2m42s
Publish Promptflow Doc / Build (push) Has been cancelled
Publish Promptflow Doc / Deploy (push) Has been cancelled
Flake8 Lint / flake8 (push) Has been cancelled
Spell check CI / Spell_Check (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:52 +08:00

250 行
10 KiB
Python

import json
import re
import sys
import time
import typing as t
from pathlib import Path
from constants import DOCUMENT_NODE, NODES_FILE_NAME, SUPPORT_FILE_TYPE, TEXT_CHUNK
from promptflow._utils.logger_utils import get_logger
def split_document(chunk_size, chunk_overlap, documents_folder, document_node_output):
try:
from llama_index import SimpleDirectoryReader
from llama_index.node_parser import SentenceSplitter
from llama_index.readers.schema import Document as LlamaindexDocument
from llama_index.schema import BaseNode
except ImportError as e:
raise ImportError(
f"{str(e)}. It appears that `llama_index` may not be installed, or the installed version may be incorrect."
"Please check `requirements.txt` file and install all the dependencies."
)
logger = get_logger("doc.split")
logger.info("Step 1: Start to split documents to document nodes...")
# count the number of files in documents_folder, including subfolders.
all_files = [f for f in Path(documents_folder).rglob("*") if f.is_file()]
filtered_num_files = sum(1 for _ in all_files if _.suffix.lower() in SUPPORT_FILE_TYPE)
logger.info(
f"Found {len(all_files)} files in the documents folder '{documents_folder}'. "
f"After filtering out unsupported file types, {filtered_num_files} files remain."
f"Using chunk size: {chunk_size} to split."
)
# `SimpleDirectoryReader` by default chunk the documents based on heading tags and paragraphs, which may lead to small chunks. # noqa: E501
reader = SimpleDirectoryReader(documents_folder, required_exts=SUPPORT_FILE_TYPE, recursive=True, encoding="utf-8")
# Disable the default suffixes to avoid splitting the documents into small chunks.
# TODO: find a better way to disable the default suffixes.
SimpleDirectoryReader.supported_suffix = []
chunks = reader.load_data()
# Convert documents into nodes
node_parser = SentenceSplitter.from_defaults(
chunk_size=chunk_size, chunk_overlap=chunk_overlap, include_metadata=True
)
chunks = t.cast(t.List[LlamaindexDocument], chunks)
document_nodes: t.List[BaseNode] = node_parser.get_nodes_from_documents(documents=chunks)
logger.info(f"Split the documents and created {len(document_nodes)} document nodes.")
document_nodes_output_path = document_node_output / Path(NODES_FILE_NAME)
with open(document_nodes_output_path, "wt") as text_file:
for doc in document_nodes:
print(json.dumps({TEXT_CHUNK: doc.text, DOCUMENT_NODE: doc.to_json()}), file=text_file)
logger.info(f"Saved document nodes to '{document_nodes_output_path}'.")
return str(Path(document_node_output) / NODES_FILE_NAME)
def clean_data(test_data_set: list, test_data_output_path: str):
logger = get_logger("data.clean")
logger.info("Step 3: Start to clean invalid test data...")
logger.info(f"Collected {len(test_data_set)} test data after the batch run.")
cleaned_data = []
for test_data in test_data_set:
if test_data and all(
val and val != "(Failed)" for key, val in test_data.items() if key.lower() != "line_number"
):
data_line = {"question": test_data["question"], "suggested_answer": test_data["suggested_answer"]}
cleaned_data.append(data_line)
jsonl_str = "\n".join(map(json.dumps, cleaned_data))
with open(test_data_output_path, "wt") as text_file:
print(f"{jsonl_str}", file=text_file)
# TODO: aggregate invalid data root cause and count, and log it.
# log debug info path.
logger.info(
f"Removed {len(test_data_set) - len(cleaned_data)} invalid test data. "
f"Saved {len(cleaned_data)} valid test data to '{test_data_output_path}'."
)
def count_non_blank_lines(file_path):
with open(file_path, "r") as file:
lines = file.readlines()
non_blank_lines = len([line for line in lines if line.strip()])
return non_blank_lines
def print_progress(log_file_path: str, process):
from tqdm import tqdm
logger = get_logger("data.gen")
finished_log_pattern = re.compile(r".*execution.bulk\s+INFO\s+Finished (\d+) / (\d+) lines\.")
progress_log_pattern = re.compile(
r".*execution.bulk\s+INFO.*\[Finished: (\d+)\] \[Processing: (\d+)\] \[Pending: (\d+)\]"
)
# wait for the log file to be created
start_time = time.time()
while not Path(log_file_path).is_file():
time.sleep(1)
# if the log file is not created within 5 minutes, raise an error
if time.time() - start_time > 300:
raise Exception(f"Log file '{log_file_path}' is not created within 5 minutes.")
logger.info(f"Click '{log_file_path}' to see detailed batch run log. Showing the progress here...")
progress_bar = None
try:
last_data_time = time.time()
with open(log_file_path, "r") as f:
while True:
status = process.poll()
# status is None if not finished, 0 if finished successfully, and non-zero if failed
if status:
stdout, _ = process.communicate()
raise Exception(f"Batch run failed due to {stdout.decode('utf-8')}")
line = f.readline().strip()
if line:
last_data_time = time.time() # Update the time when the last data was received
progress_match = progress_log_pattern.match(line)
finished_match = finished_log_pattern.match(line)
if not progress_match and not finished_match:
continue
if progress_match:
finished, processing, pending = map(int, progress_match.groups())
total = finished + processing + pending
if progress_bar is None:
# Set mininterval=0 to refresh the progress bar when it calls progress_bar.update
# after initialization.
progress_bar = tqdm(total=total, desc="Processing", mininterval=0, file=sys.stdout)
progress_bar.update(finished - progress_bar.n)
if finished_match:
finished, total = map(int, finished_match.groups())
if progress_bar is None:
progress_bar = tqdm(total=total, desc="Processing", mininterval=0, file=sys.stdout)
progress_bar.update(finished - progress_bar.n)
if finished == total:
progress_bar.close()
logger.info("Batch run is completed.")
break
elif time.time() - last_data_time > 300:
logger.info(
"No new log line received for 5 minutes. Stop reading. "
f"See the log file '{log_file_path}' for more details."
)
break
else:
time.sleep(1) # wait for 1 second if no new line is available
except Exception as e:
raise Exception(f"Error occurred while printing batch run progress: {e}.")
finally:
if progress_bar:
progress_bar.close()
def convert_to_abs_path(file_path: str) -> str:
if not file_path:
return file_path
path = Path(file_path)
if path.is_absolute():
return str(path)
elif path.exists():
abs = str(path.resolve())
return abs
else:
return file_path
def local_path_exists(path):
return Path(path).exists()
def non_padding_path(path):
return not (path.startswith("<") and path.endswith(">"))
def _retrieve_file_names_from_document_nodes_file(document_nodes_file_path) -> t.List[str]:
text_info = {}
with open(document_nodes_file_path, "r") as file:
for line in file:
# Should skip empty new lines, otherwise, json.loads would throw error.
if not line.strip():
continue
line_json = json.loads(line)
text_chunk = line_json[TEXT_CHUNK]
document_node = json.loads(line_json["document_node"])
file_path = document_node["metadata"]["file_path"]
text_info[text_chunk] = file_path
return text_info
def _count_lines(file_path) -> int:
with open(file_path, "r") as f:
return sum(1 for line in f if line.strip())
def summarize_batch_run_res(gen_details_file_path, document_nodes_file_path, output_file_path):
success_count = 0
validate_failed_count = 0
validate_failed_steps = {}
validate_failed_distribution = {}
nodes_file_lines_count = _count_lines(document_nodes_file_path)
document_nodes_info = _retrieve_file_names_from_document_nodes_file(document_nodes_file_path)
with open(gen_details_file_path, "r") as details_f:
for details_line in details_f:
# Should skip empty new lines, otherwise, json.loads would throw error.
if not details_line.strip():
continue
data = json.loads(details_line)
if data["debug_info"] == "(Failed)":
continue
if data["debug_info"]["validation_summary"]["success"]:
success_count += 1
else:
validate_failed_count += 1
failed_step = data["debug_info"]["validation_summary"]["failed_step"]
if failed_step in validate_failed_steps:
validate_failed_steps[failed_step] += 1
else:
validate_failed_steps[failed_step] = 1
validate_failed_distribution[failed_step] = {}
document_name = document_nodes_info[data["debug_info"]["text_chunk"]]
if document_name in validate_failed_distribution[failed_step]:
validate_failed_distribution[failed_step][document_name] += 1
else:
validate_failed_distribution[failed_step][document_name] = 1
data = {
"total_count": nodes_file_lines_count,
"success_count": success_count,
"run_failed_count": nodes_file_lines_count - success_count - validate_failed_count,
"validate_failed_count": validate_failed_count,
"validate_failed_steps": validate_failed_steps,
"validate_failed_distribution": validate_failed_distribution,
}
with open(output_file_path, "w") as file:
json.dump(data, file, indent=4)