Files
deepdata/my_ops/text_quality_evaluator/process.py
T
沃巴蒂 ff22ed3a8b feat(deepdata): 引入修改密码、文件预览组件与清洗算子增强,deepflow 改名为 deep-flow
本次同步 09-12 至 09-28 的代码变更,主要内容:

新增功能
- 修改密码:后端新增 ChangePasswordRequest DTO 与UserController/UserService
  对应接口,前端新增 ChangePasswordDialog 并在 Header 接入入口
- 文件预览组件重构:file-preview 目录拆分出 VideoPreview(支持 blobUrl 直传)、
  ImagePreview、AudioPreview,index.tsx 统一调度各类型预览器
- 新增 my_ops 算子模块:markitdown_parser、mineru_pdf_parser、text_quality_evaluator
- 新增 tools/minio-file-server MinIO 文件服务

功能增强
- 数据清洗:cleaning_task_service/scheduler/routes 重构,
  data_juicer_executor 与 wrapper 增强,OperatorTable/LogsTable/TaskDetail 同步调整
- 数据标注:annotation sync.py、project.py 接口与schema 更新
- 数据采集、算子市场、首页与菜单路由相关页面调整
- DataJuicer 算子接入(useOperatorOperations、dj_ops_schema)

结构调整
- deepflow/ 更名为 deep-flow/(419 个文件重命名)
- 新增 helm 部署编排、GitHub Actions 之外的自建镜像构建脚本

未纳入本次提交
- scripts-backup/、runtime/python-executor-backup/、components_backup/(备份目录)
- *.backup*、__pycache__ 产物
- zh_core_web_sm-3.8.0-py3-none-any.whl(46MB 构建产物)
- DataX/(内嵌独立仓库,远程亦未收录其源码)
2026-10-03 22:44:56 +08:00

230 lines
8.8 KiB
Python

# -*- coding: utf-8 -*-
"""
Text Quality Evaluator Process Logic
基于 LLM 的长文本多维度质量评估算子
"""
import re
import time
import json
import logging
import os
import sys
from typing import Dict, Any, List
from concurrent.futures import ThreadPoolExecutor, as_completed
try:
from deepdata.core.base_op import LLM
from deepdata.common.utils.text_splitter import TextSplitter
except ImportError:
try:
executor_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'runtime', 'python-executor'))
if executor_path not in sys.path:
sys.path.insert(0, executor_path)
from deepdata.core.base_op import LLM
from deepdata.common.utils.text_splitter import TextSplitter
except Exception as e:
print(f"{e}")
sys.exit(-1)
# 内置默认配置
DEFAULT_DIMENSIONS = '[{"name": "连贯性", "desc": "逻辑通顺度"}, {"name": "信息密度", "desc": "有效信息占比"}]'
DEFAULT_PROMPT = "请作为资深数据专家,对以下文本进行多维度质量打分(0.0~1.0):\n\n【待评估文本】\n{context}\n\n【评估维度】\n{dimensions}\n\n请严格按照格式返回对应数量的逗号分隔的浮点数,不要输出其他内容。"
class TextQualityEvaluator(LLM):
"""
文本质量评估算子
"""
def __init__(self, *args, **kwargs):
# A. 解析 Headers (必须转为 Dict,否则 requests 报错)
llm_headers = kwargs.get("LLMHeaders")
if isinstance(llm_headers, str):
try:
llm_headers = json.loads(llm_headers)
except:
llm_headers = {"Content-Type": "application/json"}
kwargs["LLMHeaders"] = llm_headers
# B. 解析 Body (必须转为 Dict)
llm_body = kwargs.get("LLMBody")
if isinstance(llm_body, str):
try:
llm_body = json.loads(llm_body)
except:
llm_body = {}
kwargs["LLMBody"] = llm_body
# C. 转换 accessType (将 "true"/"false" 转为 布尔值)
access_type = kwargs.get("accessType")
if isinstance(access_type, str):
kwargs["accessType"] = access_type.lower() == "true"
# D. 替换模型名称
model_name = kwargs.get("LLMModel", "qwen-plus")
if isinstance(llm_body, dict):
llm_body["model"] = model_name
kwargs["LLMBody"] = llm_body
# ==========================================
# 3. 调用父类初始化
# ==========================================
super(TextQualityEvaluator, self).__init__(*args, **kwargs)
self.task_id = kwargs.get("taskId", "default_id")
self.max_workers = int(kwargs.get("max_workers", 5))
self.chunk_size = int(kwargs.get("chunk_size", 2000))
self.chunk_overlap = int(kwargs.get("chunk_overlap", 100))
dim_conf = kwargs.get("dimensions")
if not dim_conf: dim_conf = DEFAULT_DIMENSIONS
if isinstance(dim_conf, str):
try: self.dimensions = json.loads(dim_conf)
except: self.dimensions = []
else: self.dimensions = dim_conf
self.prompt_template = kwargs.get("prompt_template") or DEFAULT_PROMPT
self.pattern = r'\d+\.\d+'
def _split_text(self, text: str) -> List[str]:
"""文本分块逻辑"""
if not text: return []
try:
splitter = TextSplitter(1024 * 1024 * 10, self.chunk_size, self.chunk_overlap)
return splitter.split_text(text)
except Exception:
pass
# 降级方案
chunks = []
start = 0
while start < len(text):
chunks.append(text[start : start + self.chunk_size])
start += self.chunk_size - self.chunk_overlap
return chunks
def execute(self, sample: Dict[str, Any]) -> Dict[str, Any]:
"""算子执行入口"""
start_time = time.time()
self.read_file_first(sample)
text = sample.get(getattr(self, 'text_key', 'text'), "")
if not text: return sample
# 1. 分块
chunks = self._split_text(text)
print(f"📄 文本分块完成:共 {len(chunks)} 个块")
# 2. 准备 Prompt
dim_str = "\n".join([f"- {d.get('name', 'Unknown')}: {d.get('desc', '')}" for d in self.dimensions])
prompts = [self.prompt_template.replace("{context}", c).replace("{dimensions}", dim_str) for c in chunks]
# 3. 并发调用
weighted_scores = [0.0] * len(self.dimensions)
total_chars = 0
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
future_to_idx = {executor.submit(self._call_llm_with_retry, p): i for i, p in enumerate(prompts)}
for future in as_completed(future_to_idx):
idx = future_to_idx[future]
try:
scores = future.result()
if scores:
for i, s in enumerate(scores):
weighted_scores[i] += s * len(chunks[idx])
total_chars += len(chunks[idx])
except Exception as e:
print(f"❌ 块 {idx} 失败: {e}")
# 4. 聚合
final_scores = {}
if total_chars > 0:
for i, dim in enumerate(self.dimensions):
final_scores[dim.get('name', f'Dim_{i}')] = round(weighted_scores[i] / total_chars, 4)
print(f"✅ 评估完成 | 耗时: {time.time()-start_time:.2f}s | 得分: {final_scores}")
output_record = {
"evaluation_scores": final_scores,
"dimensions_count": len(self.dimensions),
# 添加关联元数据
"source_file_id": sample.get(self.fileid_key), # 原始文件 ID
"source_file_name": sample.get(self.filename_key), # 原始文件名
"source_dataset_id": sample.get("dataset_id")
}
sample[self.text_key] = json.dumps(output_record, ensure_ascii=False)
sample["target_type"] = "jsonl"
base_name = os.path.splitext(sample.get(self.filename_key, ""))[0]
sample[self.filename_key] = f"{base_name}_eval"
'''
# 独立存储文件
try:
self.save_sample([output_record], sample)
sample_key = getattr(self, 'text_key', 'text')
sample[sample_key] = ""
except Exception as e:
print(f"Failed to save sample: {e}")
'''
return sample
def _call_llm_with_retry(self, prompt: str, retries: int = 2) -> List[float]:
retry_count = 0
while retry_count <= retries:
try:
response = self.llm(prompt)
return self._parse_scores(response)
except Exception as e:
print(f"⚠️ LLM 请求失败 ({retry_count+1}): {e}")
retry_count += 1
time.sleep(1)
return None
def _parse_scores(self, text: str) -> List[float]:
if not text: return None
matches = re.findall(self.pattern, text)
return [float(m) for m in matches if 0.0 <= float(m) <= 1.0] if matches else None
# ==========================================================
# 🧪 本地测试入口 (实际调用 Ollama)
# ==========================================================
if __name__ == "__main__":
print("🚀 启动 TextQualityEvaluator 测试...")
# 配置参数 (请确保 Ollama 正在运行且已下载模型)
mock_kwargs = {
# --- Ollama 连接配置 ---
# 注意:如果你在 Docker 容器外运行,使用 localhost;如果在容器内运行,可能需要 host.docker.internal
"LLMUrl": "http://ollama.ailab.ccoop.cc:18585/v1/chat/completions",
"accessType": "true", # 必须为 "true" 才能绕过 DeepData 内部证书校验
"isHttps": "false",
"LLMModel": "qwen2.5:7b-instruct-q4_0", # <--- 请修改为你 Ollama 中实际存在的模型
# 请求体模板
"LLMBody": '{"model": "{model}", "messages": [{"role": "user", "content": "{prompt}"}]}',
# 业务参数
"dimensions": '[{"name": "连贯性", "desc": "逻辑通顺度"}, {"name": "信息密度", "desc": "有效信息占比"}]',
"max_workers": 1
}
# 模拟输入样本
sample_text = "DeepData 是一个强大的数据处理平台,支持多种文档格式的解析与处理。" * 20
mock_sample = {"text": sample_text}
try:
# 实例化算子 (这步会触发 LLM 初始化)
evaluator = TextQualityEvaluator(**mock_kwargs)
# 执行
evaluator.execute(mock_sample)
except Exception as e:
print(f"\n❌ 测试失败: {e}")
import traceback
traceback.print_exc()