feat: 新增外部数据源拉取与 DPO 输出格式支持
- 支持从 PostgreSQL 数据库拉取结构化数据作为训练来源 - 新增 DPO (Direct Preference Optimization) 输出类型 - 支持 chosen/rejected 字段的编辑、校验和发布 - 完善数据预处理切分逻辑和元数据管理 - 移除 OCR 扫描 PDF 功能,保持基础文本解析能力 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -50,16 +50,15 @@ YG_FT/
|
|||||||
|
|
||||||
## 前后端一键启动
|
## 前后端一键启动
|
||||||
|
|
||||||
首次使用前,请先按下方“后端启动”和“前端启动”说明安装依赖,并确保
|
首次使用前请确保前端依赖已安装、PostgreSQL 已可用。之后在项目根目录执行:
|
||||||
PostgreSQL 已可用。之后在项目根目录执行:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash ./start.sh
|
bash ./start.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本会同时启动前端 `http://localhost:16801` 和后端
|
脚本会自动补装后端 `requirements.txt`,然后同时启动前端 `http://localhost:16801` 和后端
|
||||||
`http://127.0.0.1:17861`,按 `Ctrl+C` 会同时停止两个服务。脚本只负责
|
`http://127.0.0.1:17861`,按 `Ctrl+C` 会同时停止两个服务。脚本不会自动安装
|
||||||
启动前后端,不会自动安装依赖,也不会启动 PostgreSQL、Redis 或算力服务。
|
前端依赖,也不会启动 PostgreSQL、Redis 或算力服务。
|
||||||
|
|
||||||
仅检查依赖和端口而不启动服务:
|
仅检查依赖和端口而不启动服务:
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from dataclasses import asdict
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import BoundedSemaphore, Lock
|
from threading import BoundedSemaphore, Lock
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from urllib.parse import quote, urlsplit
|
from urllib.parse import parse_qs, quote, urlsplit
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import psycopg
|
import psycopg
|
||||||
@@ -676,8 +676,10 @@ def _run_generation(
|
|||||||
qa_pairs_per_item=int(pairs or 1),
|
qa_pairs_per_item=int(pairs or 1),
|
||||||
on_progress=report_progress,
|
on_progress=report_progress,
|
||||||
)
|
)
|
||||||
elif output_type == "reasoning":
|
elif output_type in {"reasoning", "dpo"}:
|
||||||
raise InvalidStateError("思维链输出必须配置可用的数据生成模型")
|
if output_type == "reasoning":
|
||||||
|
raise InvalidStateError("思维链输出必须配置可用的数据生成模型")
|
||||||
|
raise InvalidStateError("DPO 输出必须配置可用的数据生成模型")
|
||||||
else:
|
else:
|
||||||
generated = generate_standard_records(
|
generated = generate_standard_records(
|
||||||
preview_items,
|
preview_items,
|
||||||
@@ -1077,7 +1079,10 @@ async def upload_source_files(
|
|||||||
with api_errors():
|
with api_errors():
|
||||||
task = store.get_task(task_id)
|
task = store.get_task(task_id)
|
||||||
process_type = str(task["process_type"])
|
process_type = str(task["process_type"])
|
||||||
if process_type == "external":
|
source_mode = str(
|
||||||
|
_value(task.get("config") or {}, "source_mode", "sourceMode", "local")
|
||||||
|
)
|
||||||
|
if process_type == "external" or source_mode == "external":
|
||||||
raise InvalidStateError(
|
raise InvalidStateError(
|
||||||
"external tasks must import data through the external source endpoint"
|
"external tasks must import data through the external source endpoint"
|
||||||
)
|
)
|
||||||
@@ -1375,6 +1380,10 @@ def _external_postgres_connection(payload: ExternalSourceRequest) -> psycopg.Con
|
|||||||
raise fail(501, f"external data source type is not supported: {payload.type}")
|
raise fail(501, f"external data source type is not supported: {payload.type}")
|
||||||
if parsed_url.username or parsed_url.password:
|
if parsed_url.username or parsed_url.password:
|
||||||
raise fail(400, "database credentials must use the account and password fields")
|
raise fail(400, "database credentials must use the account and password fields")
|
||||||
|
if set(parse_qs(parsed_url.query)) & {
|
||||||
|
"password", "secret", "token", "api_key", "user", "username"
|
||||||
|
}:
|
||||||
|
raise fail(400, "database URL query must not contain credentials")
|
||||||
if payload.auth_mode not in {"none", "basic"}:
|
if payload.auth_mode not in {"none", "basic"}:
|
||||||
raise fail(400, "PostgreSQL supports only none or basic authentication")
|
raise fail(400, "PostgreSQL supports only none or basic authentication")
|
||||||
if payload.auth_mode == "basic" and not payload.username:
|
if payload.auth_mode == "basic" and not payload.username:
|
||||||
@@ -1413,10 +1422,14 @@ def _external_postgres_connection(payload: ExternalSourceRequest) -> psycopg.Con
|
|||||||
"set DATA_PROCESS_ALLOW_PRIVATE_EXTERNAL_DB=true only in a trusted deployment",
|
"set DATA_PROCESS_ALLOW_PRIVATE_EXTERNAL_DB=true only in a trusted deployment",
|
||||||
)
|
)
|
||||||
kwargs: dict[str, Any] = {
|
kwargs: dict[str, Any] = {
|
||||||
"connect_timeout": 5,
|
"connect_timeout": payload.connect_timeout_seconds,
|
||||||
"row_factory": dict_row,
|
"row_factory": dict_row,
|
||||||
"application_name": "yg-ft-data-process-readonly",
|
"application_name": "yg-ft-data-process-readonly",
|
||||||
"options": "-c default_transaction_read_only=on -c statement_timeout=30000",
|
"options": (
|
||||||
|
"-c default_transaction_read_only=on "
|
||||||
|
f"-c statement_timeout={payload.statement_timeout_seconds * 1000}"
|
||||||
|
),
|
||||||
|
"sslmode": payload.ssl_mode,
|
||||||
}
|
}
|
||||||
if payload.auth_mode == "basic" and payload.username:
|
if payload.auth_mode == "basic" and payload.username:
|
||||||
kwargs["user"] = payload.username
|
kwargs["user"] = payload.username
|
||||||
@@ -1425,6 +1438,17 @@ def _external_postgres_connection(payload: ExternalSourceRequest) -> psycopg.Con
|
|||||||
return psycopg.connect(payload.url, **kwargs)
|
return psycopg.connect(payload.url, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_external_source_task(task: dict[str, Any]) -> None:
|
||||||
|
if str(task.get("process_type")) == "external":
|
||||||
|
return
|
||||||
|
config = task.get("config") or {}
|
||||||
|
source_mode = str(_value(config, "source_mode", "sourceMode", "local"))
|
||||||
|
if str(task.get("process_type")) != "structured" or source_mode != "external":
|
||||||
|
raise InvalidStateError(
|
||||||
|
"external source access requires a structured task with source_mode=external"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{task_id}/external/test")
|
@router.post("/{task_id}/external/test")
|
||||||
def test_external_source(
|
def test_external_source(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
@@ -1433,10 +1457,7 @@ def test_external_source(
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
with api_errors():
|
with api_errors():
|
||||||
task = store.get_task(task_id)
|
task = store.get_task(task_id)
|
||||||
if str(task.get("process_type")) != "external":
|
_assert_external_source_task(task)
|
||||||
raise InvalidStateError(
|
|
||||||
"external source access requires an external data processing task"
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
with _external_postgres_connection(payload) as conn:
|
with _external_postgres_connection(payload) as conn:
|
||||||
conn.execute("SELECT 1 AS ok").fetchone()
|
conn.execute("SELECT 1 AS ok").fetchone()
|
||||||
@@ -1462,12 +1483,14 @@ def pull_external_source(
|
|||||||
raise fail(400, "a read-only SELECT or WITH query is required for external pull")
|
raise fail(400, "a read-only SELECT or WITH query is required for external pull")
|
||||||
with api_errors():
|
with api_errors():
|
||||||
task = store.get_task(task_id)
|
task = store.get_task(task_id)
|
||||||
if str(task.get("process_type")) != "external":
|
_assert_external_source_task(task)
|
||||||
raise InvalidStateError("external pull requires an external data processing task")
|
|
||||||
try:
|
try:
|
||||||
with _external_postgres_connection(payload) as conn:
|
with _external_postgres_connection(payload) as conn:
|
||||||
conn.execute("SET TRANSACTION READ ONLY")
|
conn.execute("SET TRANSACTION READ ONLY")
|
||||||
conn.execute("SET LOCAL statement_timeout = '30s'")
|
conn.execute(
|
||||||
|
"SELECT set_config('statement_timeout', %s, true)",
|
||||||
|
(f"{payload.statement_timeout_seconds}s",),
|
||||||
|
)
|
||||||
cursor = conn.execute(query)
|
cursor = conn.execute(query)
|
||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
content_parts: list[str] = []
|
content_parts: list[str] = []
|
||||||
@@ -1518,6 +1541,9 @@ def pull_external_source(
|
|||||||
"external_type": payload.type,
|
"external_type": payload.type,
|
||||||
"external_host": urlsplit(payload.url).hostname,
|
"external_host": urlsplit(payload.url).hostname,
|
||||||
"external_limit": payload.limit,
|
"external_limit": payload.limit,
|
||||||
|
"external_ssl_mode": payload.ssl_mode,
|
||||||
|
"external_connect_timeout_seconds": payload.connect_timeout_seconds,
|
||||||
|
"external_statement_timeout_seconds": payload.statement_timeout_seconds,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1566,7 +1592,10 @@ def _prepare_preview_items(
|
|||||||
for index, source in enumerate(sources):
|
for index, source in enumerate(sources):
|
||||||
source_format = str(source.get("file_format") or "").lower()
|
source_format = str(source.get("file_format") or "").lower()
|
||||||
needs_structured_xlsx = not is_unstructured and source_format == "xlsx"
|
needs_structured_xlsx = not is_unstructured and source_format == "xlsx"
|
||||||
needs_layout_raw = is_unstructured and chunk_method == "layout_hybrid"
|
needs_layout_raw = (
|
||||||
|
is_unstructured
|
||||||
|
and chunk_method == "layout_hybrid"
|
||||||
|
)
|
||||||
needs_pdf_noise = (
|
needs_pdf_noise = (
|
||||||
is_unstructured
|
is_unstructured
|
||||||
and not needs_layout_raw
|
and not needs_layout_raw
|
||||||
@@ -2038,6 +2067,8 @@ def restore_result(
|
|||||||
"instruction": current.get("original_instruction") or current.get("instruction") or "",
|
"instruction": current.get("original_instruction") or current.get("instruction") or "",
|
||||||
"input": current.get("original_input") or current.get("input") or "",
|
"input": current.get("original_input") or current.get("input") or "",
|
||||||
"output": current.get("original_output") or current.get("output") or "",
|
"output": current.get("original_output") or current.get("output") or "",
|
||||||
|
"chosen": current.get("original_chosen") or current.get("chosen") or "",
|
||||||
|
"rejected": current.get("original_rejected") or current.get("rejected") or "",
|
||||||
}
|
}
|
||||||
preview_id = current.get("preview_item_id")
|
preview_id = current.get("preview_item_id")
|
||||||
source_content = ""
|
source_content = ""
|
||||||
@@ -2070,6 +2101,8 @@ def restore_result(
|
|||||||
"instruction": restored["instruction"],
|
"instruction": restored["instruction"],
|
||||||
"input": restored["input"],
|
"input": restored["input"],
|
||||||
"output": restored["output"],
|
"output": restored["output"],
|
||||||
|
"chosen": restored["chosen"],
|
||||||
|
"rejected": restored["rejected"],
|
||||||
"quality_score": asdict(quality),
|
"quality_score": asdict(quality),
|
||||||
"expected_updated_at": current.get("updated_at"),
|
"expected_updated_at": current.get("updated_at"),
|
||||||
},
|
},
|
||||||
@@ -2141,13 +2174,15 @@ def _generate_result_replacement(
|
|||||||
).strip().lower()
|
).strip().lower()
|
||||||
previous_instruction = str(current.get("instruction") or "")[:1000]
|
previous_instruction = str(current.get("instruction") or "")[:1000]
|
||||||
previous_output = str(current.get("output") or "")[:1000]
|
previous_output = str(current.get("output") or "")[:1000]
|
||||||
|
previous_rejected = str(current.get("rejected") or "")[:1000]
|
||||||
base_prompt = str(
|
base_prompt = str(
|
||||||
_value(config, "generation_prompt", "generationPrompt", "") or ""
|
_value(config, "generation_prompt", "generationPrompt", "") or ""
|
||||||
)
|
)
|
||||||
regeneration_instruction = (
|
regeneration_instruction = (
|
||||||
"这是一次失败结果的重新生成。请使用新的提问角度和表达,"
|
"这是一次失败结果的重新生成。请使用新的提问角度和表达,"
|
||||||
"不要复述旧结果。旧问题:"
|
"不要复述旧结果。旧问题:"
|
||||||
f"{previous_instruction or '无'};旧答案:{previous_output or '无'}。"
|
f"{previous_instruction or '无'};旧优选答案:{previous_output or '无'};"
|
||||||
|
f"旧拒选答案:{previous_rejected or '无'}。"
|
||||||
)
|
)
|
||||||
runtime_config = {
|
runtime_config = {
|
||||||
**config,
|
**config,
|
||||||
|
|||||||
@@ -268,9 +268,13 @@ CREATE TABLE IF NOT EXISTS data_process_results (
|
|||||||
instruction TEXT NOT NULL,
|
instruction TEXT NOT NULL,
|
||||||
input TEXT NOT NULL DEFAULT '',
|
input TEXT NOT NULL DEFAULT '',
|
||||||
output TEXT NOT NULL,
|
output TEXT NOT NULL,
|
||||||
|
chosen TEXT NOT NULL DEFAULT '',
|
||||||
|
rejected TEXT NOT NULL DEFAULT '',
|
||||||
original_instruction TEXT,
|
original_instruction TEXT,
|
||||||
original_input TEXT,
|
original_input TEXT,
|
||||||
original_output TEXT,
|
original_output TEXT,
|
||||||
|
original_chosen TEXT,
|
||||||
|
original_rejected TEXT,
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'valid'
|
status VARCHAR(20) NOT NULL DEFAULT 'valid'
|
||||||
CHECK (status IN ('valid', 'modified', 'invalid')),
|
CHECK (status IN ('valid', 'modified', 'invalid')),
|
||||||
error TEXT,
|
error TEXT,
|
||||||
@@ -280,6 +284,11 @@ CREATE TABLE IF NOT EXISTS data_process_results (
|
|||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE data_process_results ADD COLUMN IF NOT EXISTS chosen TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE data_process_results ADD COLUMN IF NOT EXISTS rejected TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE data_process_results ADD COLUMN IF NOT EXISTS original_chosen TEXT;
|
||||||
|
ALTER TABLE data_process_results ADD COLUMN IF NOT EXISTS original_rejected TEXT;
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_data_process_results_task_status
|
CREATE INDEX IF NOT EXISTS idx_data_process_results_task_status
|
||||||
ON data_process_results(task_id, status, id);
|
ON data_process_results(task_id, status, id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_data_process_results_task_split
|
CREATE INDEX IF NOT EXISTS idx_data_process_results_task_split
|
||||||
|
|||||||
@@ -57,7 +57,62 @@ def _sentence_chunks(text: str) -> list[str]:
|
|||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def _tokenizer() -> tiktoken.Encoding:
|
def _tokenizer() -> tiktoken.Encoding:
|
||||||
return tiktoken.get_encoding("cl100k_base")
|
"""加载 cl100k_base 编码器,优先在线下载,失败时使用本地缓存以支持离线环境。"""
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# 先设置缓存目录环境变量
|
||||||
|
offline_cache = os.path.expanduser("~/.cache/tiktoken")
|
||||||
|
os.environ.setdefault("TIKTOKEN_CACHE_DIR", offline_cache)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 尝试标准方式加载
|
||||||
|
return tiktoken.get_encoding("cl100k_base")
|
||||||
|
except Exception:
|
||||||
|
# 如果失败,尝试手动从本地文件构造
|
||||||
|
try:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
local_file = Path(offline_cache) / "9b5ad71b2ce5302211f9c61530b329a4922fc6a4"
|
||||||
|
if not local_file.exists():
|
||||||
|
# 尝试另一个可能的文件名
|
||||||
|
local_file = Path(offline_cache) / "cl100k_base.tiktoken"
|
||||||
|
|
||||||
|
if local_file.exists():
|
||||||
|
# 读取 BPE 文件内容
|
||||||
|
with open(local_file, "rb") as f:
|
||||||
|
contents = f.read()
|
||||||
|
|
||||||
|
# 解析 BPE 文件
|
||||||
|
mergeable_ranks = {}
|
||||||
|
for line in contents.splitlines():
|
||||||
|
if line:
|
||||||
|
token, rank = line.split()
|
||||||
|
mergeable_ranks[base64.b64decode(token)] = int(rank)
|
||||||
|
|
||||||
|
# 构造 Encoding 对象
|
||||||
|
import tiktoken.core
|
||||||
|
return tiktoken.core.Encoding(
|
||||||
|
name="cl100k_base",
|
||||||
|
pat_str=r"""'(?i:[sdmt]|ll|ve|re)|[^\r\n\p{L}\p{N}]?+\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]++[\r\n]*|\s*[\r\n]|\s+(?!\S)|\s+""",
|
||||||
|
mergeable_ranks=mergeable_ranks,
|
||||||
|
special_tokens={
|
||||||
|
"<|endoftext|>": 100257,
|
||||||
|
"<|fim_prefix|>": 100258,
|
||||||
|
"<|fim_middle|>": 100259,
|
||||||
|
"<|fim_suffix|>": 100260,
|
||||||
|
"<|endofprompt|>": 100276,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
f"无法加载 cl100k_base 编码器\n"
|
||||||
|
f"请确保以下任一条件满足:\n"
|
||||||
|
f"1. 服务器可以访问网络\n"
|
||||||
|
f"2. 本地存在缓存文件: {offline_cache}/9b5ad71b2ce5302211f9c61530b329a4922fc6a4"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _text_chunks(
|
def _text_chunks(
|
||||||
|
|||||||
BIN
backend/app/modules/data_process/document_chunking.txt
Normal file
BIN
backend/app/modules/data_process/document_chunking.txt
Normal file
Binary file not shown.
@@ -29,7 +29,12 @@ class _TerminalModelGenerationError(ModelGenerationError):
|
|||||||
|
|
||||||
OUTPUT_TYPE_STANDARD = "standard"
|
OUTPUT_TYPE_STANDARD = "standard"
|
||||||
OUTPUT_TYPE_REASONING = "reasoning"
|
OUTPUT_TYPE_REASONING = "reasoning"
|
||||||
SUPPORTED_OUTPUT_TYPES = {OUTPUT_TYPE_STANDARD, OUTPUT_TYPE_REASONING}
|
OUTPUT_TYPE_DPO = "dpo"
|
||||||
|
SUPPORTED_OUTPUT_TYPES = {
|
||||||
|
OUTPUT_TYPE_STANDARD,
|
||||||
|
OUTPUT_TYPE_REASONING,
|
||||||
|
OUTPUT_TYPE_DPO,
|
||||||
|
}
|
||||||
REASONING_DETAIL_NORMAL = "normal"
|
REASONING_DETAIL_NORMAL = "normal"
|
||||||
REASONING_DETAIL_DETAILED = "detailed"
|
REASONING_DETAIL_DETAILED = "detailed"
|
||||||
SUPPORTED_REASONING_DETAILS = {
|
SUPPORTED_REASONING_DETAILS = {
|
||||||
@@ -285,6 +290,18 @@ def _prompt_messages(
|
|||||||
"这是思维链输出模式,即使其他提示语要求省略分析,也不得省略 reasoning。"
|
"这是思维链输出模式,即使其他提示语要求省略分析,也不得省略 reasoning。"
|
||||||
"不要自行添加 <think> 标签,系统会在保存时统一组装。"
|
"不要自行添加 <think> 标签,系统会在保存时统一组装。"
|
||||||
)
|
)
|
||||||
|
elif output_type == OUTPUT_TYPE_DPO:
|
||||||
|
schema = (
|
||||||
|
'{"items":[{"instruction":"...","input":"...",'
|
||||||
|
'"chosen":"...","rejected":"..."}]}'
|
||||||
|
)
|
||||||
|
output_rule = (
|
||||||
|
"你正在生成用于直接偏好优化(DPO)的成对偏好数据。"
|
||||||
|
"instruction、chosen 和 rejected 均不得为空;chosen 必须是忠于来源、"
|
||||||
|
"准确完整的优选回答,rejected 必须是表面合理但存在明确质量缺陷的拒选回答。"
|
||||||
|
"两者不得相同;rejected 不得包含违法危险内容,也不得用空白、乱码或无关文本凑数。"
|
||||||
|
"不要输出分析过程或 <think> 标签。"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
schema = '{"items":[{"instruction":"...","input":"...","output":"..."}]}'
|
schema = '{"items":[{"instruction":"...","input":"...","output":"..."}]}'
|
||||||
output_rule = (
|
output_rule = (
|
||||||
@@ -461,9 +478,13 @@ def generate_model_records(
|
|||||||
"instruction": failure_instruction,
|
"instruction": failure_instruction,
|
||||||
"input": content,
|
"input": content,
|
||||||
"output": "",
|
"output": "",
|
||||||
|
"chosen": "",
|
||||||
|
"rejected": "",
|
||||||
"original_instruction": failure_instruction,
|
"original_instruction": failure_instruction,
|
||||||
"original_input": content,
|
"original_input": content,
|
||||||
"original_output": "",
|
"original_output": "",
|
||||||
|
"original_chosen": "",
|
||||||
|
"original_rejected": "",
|
||||||
"status": "invalid",
|
"status": "invalid",
|
||||||
"error": error_message,
|
"error": error_message,
|
||||||
"split": "train",
|
"split": "train",
|
||||||
@@ -479,6 +500,8 @@ def generate_model_records(
|
|||||||
input_text = normalize_text(
|
input_text = normalize_text(
|
||||||
str(value.get("input") or value.get("context") or "")
|
str(value.get("input") or value.get("context") or "")
|
||||||
)
|
)
|
||||||
|
chosen = ""
|
||||||
|
rejected = ""
|
||||||
if output_type == OUTPUT_TYPE_REASONING:
|
if output_type == OUTPUT_TYPE_REASONING:
|
||||||
reasoning = normalize_text(
|
reasoning = normalize_text(
|
||||||
re.sub(
|
re.sub(
|
||||||
@@ -508,6 +531,36 @@ def generate_model_records(
|
|||||||
)
|
)
|
||||||
valid = bool(instruction and reasoning and answer)
|
valid = bool(instruction and reasoning and answer)
|
||||||
missing_error = "model result is missing instruction, reasoning or answer"
|
missing_error = "model result is missing instruction, reasoning or answer"
|
||||||
|
elif output_type == OUTPUT_TYPE_DPO:
|
||||||
|
chosen = normalize_text(str(value.get("chosen") or ""))
|
||||||
|
rejected = normalize_text(str(value.get("rejected") or ""))
|
||||||
|
chosen = normalize_text(
|
||||||
|
re.sub(
|
||||||
|
r"<think>[\s\S]*?(?:</think>|$)",
|
||||||
|
"",
|
||||||
|
chosen,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rejected = normalize_text(
|
||||||
|
re.sub(
|
||||||
|
r"<think>[\s\S]*?(?:</think>|$)",
|
||||||
|
"",
|
||||||
|
rejected,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
output = chosen
|
||||||
|
valid = bool(
|
||||||
|
instruction
|
||||||
|
and chosen
|
||||||
|
and rejected
|
||||||
|
and chosen.strip() != rejected.strip()
|
||||||
|
)
|
||||||
|
missing_error = (
|
||||||
|
"model result is missing instruction, chosen or rejected, "
|
||||||
|
"or chosen equals rejected"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
output = normalize_text(
|
output = normalize_text(
|
||||||
str(
|
str(
|
||||||
@@ -527,7 +580,10 @@ def generate_model_records(
|
|||||||
)
|
)
|
||||||
valid = bool(instruction and output)
|
valid = bool(instruction and output)
|
||||||
missing_error = "model result is missing instruction or output"
|
missing_error = "model result is missing instruction or output"
|
||||||
raw_id = f"{preview_id}:{variant_index + 1}:{instruction}:{output}"
|
raw_id = (
|
||||||
|
f"{preview_id}:{variant_index + 1}:{instruction}:"
|
||||||
|
f"{output}:{rejected}"
|
||||||
|
)
|
||||||
result_id = f"result_{hashlib.sha256(raw_id.encode()).hexdigest()[:16]}"
|
result_id = f"result_{hashlib.sha256(raw_id.encode()).hexdigest()[:16]}"
|
||||||
results.append(
|
results.append(
|
||||||
{
|
{
|
||||||
@@ -536,9 +592,13 @@ def generate_model_records(
|
|||||||
"instruction": instruction,
|
"instruction": instruction,
|
||||||
"input": input_text,
|
"input": input_text,
|
||||||
"output": output,
|
"output": output,
|
||||||
|
"chosen": chosen,
|
||||||
|
"rejected": rejected,
|
||||||
"original_instruction": instruction,
|
"original_instruction": instruction,
|
||||||
"original_input": input_text,
|
"original_input": input_text,
|
||||||
"original_output": output,
|
"original_output": output,
|
||||||
|
"original_chosen": chosen,
|
||||||
|
"original_rejected": rejected,
|
||||||
"status": "valid" if valid else "invalid",
|
"status": "valid" if valid else "invalid",
|
||||||
"error": (None if valid else missing_error),
|
"error": (None if valid else missing_error),
|
||||||
"split": "train",
|
"split": "train",
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ def _reasoning_output_is_valid(value: Any) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _dpo_fields_are_valid(row: dict[str, Any]) -> bool:
|
||||||
|
chosen = str(row.get("chosen") or "").strip()
|
||||||
|
rejected = str(row.get("rejected") or "").strip()
|
||||||
|
return bool(chosen and rejected and chosen != rejected)
|
||||||
|
|
||||||
|
|
||||||
def _preview_config_value(config: dict[str, Any], key: str, default: Any) -> Any:
|
def _preview_config_value(config: dict[str, Any], key: str, default: Any) -> Any:
|
||||||
if key in config:
|
if key in config:
|
||||||
return config[key]
|
return config[key]
|
||||||
@@ -846,7 +852,11 @@ class DataProcessStore:
|
|||||||
)
|
)
|
||||||
instruction = str(record.get("instruction") or raw.get("instruction") or "")
|
instruction = str(record.get("instruction") or raw.get("instruction") or "")
|
||||||
input_text = str(record.get("input") or raw.get("input") or "")
|
input_text = str(record.get("input") or raw.get("input") or "")
|
||||||
output = str(record.get("output") or raw.get("output") or "")
|
chosen = str(raw.get("chosen") or "")
|
||||||
|
rejected = str(raw.get("rejected") or "")
|
||||||
|
output = str(
|
||||||
|
record.get("output") or raw.get("output") or chosen or ""
|
||||||
|
)
|
||||||
split = str(record.get("split") or raw.get("split") or "") or None
|
split = str(record.get("split") or raw.get("split") or "") or None
|
||||||
status = str(record.get("status") or "valid")
|
status = str(record.get("status") or "valid")
|
||||||
if status not in {"valid", "modified", "invalid"}:
|
if status not in {"valid", "modified", "invalid"}:
|
||||||
@@ -856,10 +866,11 @@ class DataProcessStore:
|
|||||||
"""
|
"""
|
||||||
INSERT INTO data_process_results
|
INSERT INTO data_process_results
|
||||||
(id, task_id, preview_item_id, instruction, input, output,
|
(id, task_id, preview_item_id, instruction, input, output,
|
||||||
original_instruction, original_input, original_output, status,
|
chosen, rejected, original_instruction, original_input,
|
||||||
|
original_output, original_chosen, original_rejected, status,
|
||||||
error, split, quality_score, created_at, updated_at)
|
error, split, quality_score, created_at, updated_at)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
||||||
NULL, %s, '{}', %s, %s)
|
%s, %s, NULL, %s, '{}', %s, %s)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
result_id,
|
result_id,
|
||||||
@@ -868,9 +879,13 @@ class DataProcessStore:
|
|||||||
instruction,
|
instruction,
|
||||||
input_text,
|
input_text,
|
||||||
output,
|
output,
|
||||||
|
chosen,
|
||||||
|
rejected,
|
||||||
instruction,
|
instruction,
|
||||||
input_text,
|
input_text,
|
||||||
output,
|
output,
|
||||||
|
chosen,
|
||||||
|
rejected,
|
||||||
status,
|
status,
|
||||||
split,
|
split,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -2024,9 +2039,11 @@ class DataProcessStore:
|
|||||||
"""
|
"""
|
||||||
INSERT INTO data_process_results
|
INSERT INTO data_process_results
|
||||||
(id, task_id, preview_item_id, instruction, input, output,
|
(id, task_id, preview_item_id, instruction, input, output,
|
||||||
original_instruction, original_input, original_output, status, error,
|
chosen, rejected, original_instruction, original_input,
|
||||||
|
original_output, original_chosen, original_rejected, status, error,
|
||||||
split, quality_score, created_at, updated_at)
|
split, quality_score, created_at, updated_at)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
||||||
|
%s, %s, %s, %s, %s, %s, %s)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
result.get("id") or new_id("dpr"),
|
result.get("id") or new_id("dpr"),
|
||||||
@@ -2035,9 +2052,13 @@ class DataProcessStore:
|
|||||||
result.get("instruction") or "",
|
result.get("instruction") or "",
|
||||||
result.get("input") or "",
|
result.get("input") or "",
|
||||||
result.get("output") or "",
|
result.get("output") or "",
|
||||||
|
result.get("chosen") or "",
|
||||||
|
result.get("rejected") or "",
|
||||||
result.get("original_instruction", result.get("instruction") or ""),
|
result.get("original_instruction", result.get("instruction") or ""),
|
||||||
result.get("original_input", result.get("input") or ""),
|
result.get("original_input", result.get("input") or ""),
|
||||||
result.get("original_output", result.get("output") or ""),
|
result.get("original_output", result.get("output") or ""),
|
||||||
|
result.get("original_chosen", result.get("chosen") or ""),
|
||||||
|
result.get("original_rejected", result.get("rejected") or ""),
|
||||||
result.get("status") or "valid",
|
result.get("status") or "valid",
|
||||||
result.get("error"),
|
result.get("error"),
|
||||||
result.get("split"),
|
result.get("split"),
|
||||||
@@ -2121,7 +2142,7 @@ class DataProcessStore:
|
|||||||
|
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT status, instruction, output
|
SELECT status, instruction, output, chosen, rejected
|
||||||
FROM data_process_results
|
FROM data_process_results
|
||||||
WHERE task_id=%s
|
WHERE task_id=%s
|
||||||
""",
|
""",
|
||||||
@@ -2139,6 +2160,10 @@ class DataProcessStore:
|
|||||||
_task_output_type(task) == "reasoning"
|
_task_output_type(task) == "reasoning"
|
||||||
and not _reasoning_output_is_valid(row.get("output"))
|
and not _reasoning_output_is_valid(row.get("output"))
|
||||||
)
|
)
|
||||||
|
or (
|
||||||
|
_task_output_type(task) == "dpo"
|
||||||
|
and not _dpo_fields_are_valid(row)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if invalid_count:
|
if invalid_count:
|
||||||
raise InvalidStateError(
|
raise InvalidStateError(
|
||||||
@@ -2210,7 +2235,9 @@ class DataProcessStore:
|
|||||||
def update_result(
|
def update_result(
|
||||||
self, task_id: str, result_id: str, payload: dict[str, Any]
|
self, task_id: str, result_id: str, payload: dict[str, Any]
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
allowed = {"instruction", "input", "output", "quality_score"}
|
allowed = {
|
||||||
|
"instruction", "input", "output", "chosen", "rejected", "quality_score"
|
||||||
|
}
|
||||||
values = {key: value for key, value in payload.items() if key in allowed}
|
values = {key: value for key, value in payload.items() if key in allowed}
|
||||||
if "quality_score" in values:
|
if "quality_score" in values:
|
||||||
values["quality_score"] = json_dumps(values["quality_score"])
|
values["quality_score"] = json_dumps(values["quality_score"])
|
||||||
@@ -2232,20 +2259,28 @@ class DataProcessStore:
|
|||||||
current_updated_at = _serialize_value(current.get("updated_at"))
|
current_updated_at = _serialize_value(current.get("updated_at"))
|
||||||
if expected_updated_at and expected_updated_at != current_updated_at:
|
if expected_updated_at and expected_updated_at != current_updated_at:
|
||||||
raise ConflictError("data process result was modified by another request")
|
raise ConflictError("data process result was modified by another request")
|
||||||
|
output_type = _task_output_type(task)
|
||||||
|
if output_type == "dpo" and "chosen" in values:
|
||||||
|
values["output"] = values["chosen"]
|
||||||
merged = {**current, **values}
|
merged = {**current, **values}
|
||||||
quality = payload.get("quality_score") or {}
|
quality = payload.get("quality_score") or {}
|
||||||
instruction_valid = bool(str(merged.get("instruction") or "").strip())
|
instruction_valid = bool(str(merged.get("instruction") or "").strip())
|
||||||
output_valid = bool(str(merged.get("output") or "").strip())
|
output_valid = bool(str(merged.get("output") or "").strip())
|
||||||
reasoning_valid = (
|
reasoning_valid = (
|
||||||
_task_output_type(task) != "reasoning"
|
output_type != "reasoning"
|
||||||
or _reasoning_output_is_valid(merged.get("output"))
|
or _reasoning_output_is_valid(merged.get("output"))
|
||||||
)
|
)
|
||||||
hard_valid = instruction_valid and output_valid and reasoning_valid
|
dpo_valid = output_type != "dpo" or _dpo_fields_are_valid(merged)
|
||||||
|
hard_valid = instruction_valid and output_valid and reasoning_valid and dpo_valid
|
||||||
quality_valid = bool(quality.get("is_valid", hard_valid))
|
quality_valid = bool(quality.get("is_valid", hard_valid))
|
||||||
changed = any(
|
changed = any(
|
||||||
str(merged.get(field) or "")
|
str(merged.get(field) or "")
|
||||||
!= str(merged.get(f"original_{field}") or "")
|
!= str(merged.get(f"original_{field}") or "")
|
||||||
for field in ("instruction", "input", "output")
|
for field in (
|
||||||
|
("instruction", "input", "chosen", "rejected")
|
||||||
|
if output_type == "dpo"
|
||||||
|
else ("instruction", "input", "output")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
status = "invalid" if not hard_valid or not quality_valid else (
|
status = "invalid" if not hard_valid or not quality_valid else (
|
||||||
"modified" if changed else "valid"
|
"modified" if changed else "valid"
|
||||||
@@ -2255,6 +2290,8 @@ class DataProcessStore:
|
|||||||
format_error = (
|
format_error = (
|
||||||
"思维链输出必须包含非空的 <think>...</think> 推理过程和最终答案"
|
"思维链输出必须包含非空的 <think>...</think> 推理过程和最终答案"
|
||||||
if instruction_valid and output_valid and not reasoning_valid
|
if instruction_valid and output_valid and not reasoning_valid
|
||||||
|
else "DPO 输出必须包含不同的非空 Chosen 和 Rejected 回答"
|
||||||
|
if instruction_valid and not dpo_valid
|
||||||
else "Instruction 和 Output 不能为空"
|
else "Instruction 和 Output 不能为空"
|
||||||
if not instruction_valid or not output_valid
|
if not instruction_valid or not output_valid
|
||||||
else None
|
else None
|
||||||
@@ -2318,18 +2355,23 @@ class DataProcessStore:
|
|||||||
instruction = str(replacement.get("instruction") or "").strip()
|
instruction = str(replacement.get("instruction") or "").strip()
|
||||||
input_text = str(replacement.get("input") or "").strip()
|
input_text = str(replacement.get("input") or "").strip()
|
||||||
output = str(replacement.get("output") or "").strip()
|
output = str(replacement.get("output") or "").strip()
|
||||||
|
chosen = str(replacement.get("chosen") or "").strip()
|
||||||
|
rejected = str(replacement.get("rejected") or "").strip()
|
||||||
quality_score = replacement.get("quality_score") or {}
|
quality_score = replacement.get("quality_score") or {}
|
||||||
if not instruction or not output or not bool(quality_score.get("is_valid")):
|
if not instruction or not output or not bool(quality_score.get("is_valid")):
|
||||||
raise InvalidStateError("regenerated result did not pass quality validation")
|
raise InvalidStateError("regenerated result did not pass quality validation")
|
||||||
if _task_output_type(task) == "reasoning" and not _reasoning_output_is_valid(output):
|
if _task_output_type(task) == "reasoning" and not _reasoning_output_is_valid(output):
|
||||||
raise InvalidStateError("regenerated reasoning result has an invalid output format")
|
raise InvalidStateError("regenerated reasoning result has an invalid output format")
|
||||||
|
if _task_output_type(task) == "dpo" and not _dpo_fields_are_valid(replacement):
|
||||||
|
raise InvalidStateError("regenerated DPO result has invalid preference fields")
|
||||||
|
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE data_process_results
|
UPDATE data_process_results
|
||||||
SET instruction=%s, input=%s, output=%s,
|
SET instruction=%s, input=%s, output=%s, chosen=%s, rejected=%s,
|
||||||
original_instruction=%s, original_input=%s, original_output=%s,
|
original_instruction=%s, original_input=%s, original_output=%s,
|
||||||
|
original_chosen=%s, original_rejected=%s,
|
||||||
status='valid', error=NULL, quality_score=%s, updated_at=%s
|
status='valid', error=NULL, quality_score=%s, updated_at=%s
|
||||||
WHERE id=%s AND task_id=%s
|
WHERE id=%s AND task_id=%s
|
||||||
RETURNING *
|
RETURNING *
|
||||||
@@ -2338,9 +2380,13 @@ class DataProcessStore:
|
|||||||
instruction,
|
instruction,
|
||||||
input_text,
|
input_text,
|
||||||
output,
|
output,
|
||||||
|
chosen,
|
||||||
|
rejected,
|
||||||
instruction,
|
instruction,
|
||||||
input_text,
|
input_text,
|
||||||
output,
|
output,
|
||||||
|
chosen,
|
||||||
|
rejected,
|
||||||
json_dumps(quality_score),
|
json_dumps(quality_score),
|
||||||
now,
|
now,
|
||||||
result_id,
|
result_id,
|
||||||
@@ -2434,6 +2480,10 @@ class DataProcessStore:
|
|||||||
_task_output_type(task) == "reasoning"
|
_task_output_type(task) == "reasoning"
|
||||||
and not _reasoning_output_is_valid(row.get("output"))
|
and not _reasoning_output_is_valid(row.get("output"))
|
||||||
)
|
)
|
||||||
|
or (
|
||||||
|
_task_output_type(task) == "dpo"
|
||||||
|
and not _dpo_fields_are_valid(row)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if invalid_count:
|
if invalid_count:
|
||||||
raise InvalidStateError(f"task contains {invalid_count} invalid results")
|
raise InvalidStateError(f"task contains {invalid_count} invalid results")
|
||||||
@@ -2449,15 +2499,27 @@ class DataProcessStore:
|
|||||||
requested_split,
|
requested_split,
|
||||||
seed=task_id,
|
seed=task_id,
|
||||||
)
|
)
|
||||||
records = [
|
if _task_output_type(task) == "dpo":
|
||||||
{
|
records = [
|
||||||
"instruction": row["instruction"],
|
{
|
||||||
"input": row["input"],
|
"instruction": row["instruction"],
|
||||||
"output": row["output"],
|
"input": row["input"],
|
||||||
"split": assignment,
|
"chosen": row["chosen"],
|
||||||
}
|
"rejected": row["rejected"],
|
||||||
for row, assignment in zip(rows, assignments, strict=True)
|
"split": assignment,
|
||||||
]
|
}
|
||||||
|
for row, assignment in zip(rows, assignments, strict=True)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
records = [
|
||||||
|
{
|
||||||
|
"instruction": row["instruction"],
|
||||||
|
"input": row["input"],
|
||||||
|
"output": row["output"],
|
||||||
|
"split": assignment,
|
||||||
|
}
|
||||||
|
for row, assignment in zip(rows, assignments, strict=True)
|
||||||
|
]
|
||||||
split_order = ("train", "validation", "test")
|
split_order = ("train", "validation", "test")
|
||||||
split_counts = {
|
split_counts = {
|
||||||
split_name: assignments.count(split_name) for split_name in split_order
|
split_name: assignments.count(split_name) for split_name in split_order
|
||||||
@@ -2498,7 +2560,11 @@ class DataProcessStore:
|
|||||||
"reasoning_detail": _task_reasoning_detail(task),
|
"reasoning_detail": _task_reasoning_detail(task),
|
||||||
"source_file_ids": [item["id"] for item in self._source_ids(conn, task_id)],
|
"source_file_ids": [item["id"] for item in self._source_ids(conn, task_id)],
|
||||||
"source_result_ids": source_result_ids,
|
"source_result_ids": source_result_ids,
|
||||||
"format": payload.get("format") or "alpaca_jsonl",
|
"format": (
|
||||||
|
"dpo"
|
||||||
|
if _task_output_type(task) == "dpo"
|
||||||
|
else payload.get("format") or "alpaca_jsonl"
|
||||||
|
),
|
||||||
"split": requested_split,
|
"split": requested_split,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2741,7 +2807,7 @@ class DataProcessStore:
|
|||||||
record["split"],
|
record["split"],
|
||||||
record["instruction"],
|
record["instruction"],
|
||||||
record["input"],
|
record["input"],
|
||||||
record["output"],
|
record.get("output") or record.get("chosen") or "",
|
||||||
json_dumps(
|
json_dumps(
|
||||||
{
|
{
|
||||||
**record,
|
**record,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
from urllib.parse import parse_qs, urlsplit
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||||
|
|
||||||
@@ -15,6 +16,31 @@ def _config_value(config: dict[str, Any], snake_name: str, camel_name: str, defa
|
|||||||
|
|
||||||
|
|
||||||
def _validate_process_config(config: dict[str, Any]) -> None:
|
def _validate_process_config(config: dict[str, Any]) -> None:
|
||||||
|
output_type = _config_value(config, "output_type", "outputType", "standard")
|
||||||
|
if output_type not in {"standard", "reasoning", "dpo"}:
|
||||||
|
raise ValueError("output_type must be one of: standard, reasoning, dpo")
|
||||||
|
|
||||||
|
source_mode = _config_value(config, "source_mode", "sourceMode", "local")
|
||||||
|
if source_mode not in {"local", "external"}:
|
||||||
|
raise ValueError("source_mode must be one of: local, external")
|
||||||
|
external_source = _config_value(config, "external_source", "externalSource", None)
|
||||||
|
if external_source is not None:
|
||||||
|
if not isinstance(external_source, dict):
|
||||||
|
raise ValueError("external_source must be an object")
|
||||||
|
if any(
|
||||||
|
key.lower() in {"password", "secret", "token", "api_key"}
|
||||||
|
for key in external_source
|
||||||
|
):
|
||||||
|
raise ValueError("external_source must not persist credentials")
|
||||||
|
external_url = str(external_source.get("url") or "").strip()
|
||||||
|
if external_url:
|
||||||
|
parsed_external_url = urlsplit(external_url)
|
||||||
|
sensitive_query_keys = {"password", "secret", "token", "api_key", "user", "username"}
|
||||||
|
if parsed_external_url.username or parsed_external_url.password or (
|
||||||
|
set(parse_qs(parsed_external_url.query)) & sensitive_query_keys
|
||||||
|
):
|
||||||
|
raise ValueError("external_source URL must not contain credentials")
|
||||||
|
|
||||||
chunk_method = _config_value(config, "chunk_method", "chunkMethod", "layout_hybrid")
|
chunk_method = _config_value(config, "chunk_method", "chunkMethod", "layout_hybrid")
|
||||||
if not isinstance(chunk_method, str) or chunk_method not in {
|
if not isinstance(chunk_method, str) or chunk_method not in {
|
||||||
"layout_hybrid",
|
"layout_hybrid",
|
||||||
@@ -303,6 +329,9 @@ class ExternalSourceRequest(BaseModel):
|
|||||||
username: str | None = Field(default=None, max_length=150)
|
username: str | None = Field(default=None, max_length=150)
|
||||||
password: str | None = Field(default=None, max_length=500)
|
password: str | None = Field(default=None, max_length=500)
|
||||||
limit: int = Field(default=1000, ge=1, le=100_000)
|
limit: int = Field(default=1000, ge=1, le=100_000)
|
||||||
|
connect_timeout_seconds: int = Field(default=5, ge=1, le=30)
|
||||||
|
statement_timeout_seconds: int = Field(default=30, ge=1, le=300)
|
||||||
|
ssl_mode: Literal["disable", "prefer", "require", "verify-ca", "verify-full"] = "prefer"
|
||||||
|
|
||||||
|
|
||||||
class ExternalPullRequest(ExternalSourceRequest):
|
class ExternalPullRequest(ExternalSourceRequest):
|
||||||
@@ -324,6 +353,8 @@ class ResultUpdate(BaseModel):
|
|||||||
instruction: str | None = None
|
instruction: str | None = None
|
||||||
input: str | None = None
|
input: str | None = None
|
||||||
output: str | None = None
|
output: str | None = None
|
||||||
|
chosen: str | None = None
|
||||||
|
rejected: str | None = None
|
||||||
expected_updated_at: str | None = None
|
expected_updated_at: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -374,7 +405,7 @@ class PublishRequest(BaseModel):
|
|||||||
dataset_type: Literal["train", "test", "eval", "val", "other"] = "train"
|
dataset_type: Literal["train", "test", "eval", "val", "other"] = "train"
|
||||||
storage_type: Literal["local"] = "local"
|
storage_type: Literal["local"] = "local"
|
||||||
split: DatasetSplit = Field(default_factory=DatasetSplit)
|
split: DatasetSplit = Field(default_factory=DatasetSplit)
|
||||||
format: Literal["alpaca_jsonl", "jsonl"] = "alpaca_jsonl"
|
format: Literal["alpaca_jsonl", "jsonl", "dpo"] = "alpaca_jsonl"
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
@field_validator("dataset_name")
|
@field_validator("dataset_name")
|
||||||
|
|||||||
@@ -1840,6 +1840,43 @@ def test_external_source_never_returns_fake_success(tmp_path: Path) -> None:
|
|||||||
assert response.json()["detail"]["code"] == 501
|
assert response.json()["detail"]["code"] == 501
|
||||||
|
|
||||||
|
|
||||||
|
def test_external_source_mode_belongs_to_step_three_structured_task(tmp_path: Path) -> None:
|
||||||
|
client, _, _ = make_client(tmp_path)
|
||||||
|
local_task_id = client.post(
|
||||||
|
"/modelTF/data-process",
|
||||||
|
json={
|
||||||
|
"name": "本地结构化任务",
|
||||||
|
"process_type": "structured",
|
||||||
|
"config": {"source_mode": "local"},
|
||||||
|
},
|
||||||
|
).json()["data"]["id"]
|
||||||
|
rejected = client.post(
|
||||||
|
f"/modelTF/data-process/{local_task_id}/external/test",
|
||||||
|
json={"type": "mysql", "url": "mysql://db.example/test"},
|
||||||
|
)
|
||||||
|
assert rejected.status_code == 409
|
||||||
|
|
||||||
|
external_task_id = client.post(
|
||||||
|
"/modelTF/data-process",
|
||||||
|
json={
|
||||||
|
"name": "外部结构化任务",
|
||||||
|
"process_type": "structured",
|
||||||
|
"config": {"source_mode": "external"},
|
||||||
|
},
|
||||||
|
).json()["data"]["id"]
|
||||||
|
accepted_as_external = client.post(
|
||||||
|
f"/modelTF/data-process/{external_task_id}/external/test",
|
||||||
|
json={"type": "mysql", "url": "mysql://db.example/test"},
|
||||||
|
)
|
||||||
|
assert accepted_as_external.status_code == 501
|
||||||
|
|
||||||
|
local_upload = client.post(
|
||||||
|
f"/modelTF/data-process/{external_task_id}/source-files",
|
||||||
|
files={"files": ("records.jsonl", b'{"id":1}\n', "application/jsonl")},
|
||||||
|
)
|
||||||
|
assert local_upload.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
def test_regenerate_endpoint_prepares_an_existing_published_task(tmp_path: Path) -> None:
|
def test_regenerate_endpoint_prepares_an_existing_published_task(tmp_path: Path) -> None:
|
||||||
client, store, _ = make_client(tmp_path)
|
client, store, _ = make_client(tmp_path)
|
||||||
task_id = client.post(
|
task_id = client.post(
|
||||||
|
|||||||
@@ -88,6 +88,74 @@ def test_generate_model_records_uses_prompt_auth_and_stable_split() -> None:
|
|||||||
assert progress_updates == [(1, 1)]
|
assert progress_updates == [(1, 1)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_model_records_builds_native_dpo_pair() -> None:
|
||||||
|
def handler(request: httpx.Request) -> httpx.Response:
|
||||||
|
payload = json.loads(request.content)
|
||||||
|
system_prompt = payload["messages"][0]["content"]
|
||||||
|
assert '"chosen"' in system_prompt
|
||||||
|
assert '"rejected"' in system_prompt
|
||||||
|
assert "直接偏好优化" in system_prompt
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"choices": [{
|
||||||
|
"message": {
|
||||||
|
"content": json.dumps({
|
||||||
|
"items": [{
|
||||||
|
"instruction": "系统如何处理扫描 PDF?",
|
||||||
|
"input": "",
|
||||||
|
"chosen": "仅在没有文本层时调用 OCR,并保留页码。",
|
||||||
|
"rejected": "所有 PDF 都重复执行 OCR。",
|
||||||
|
}],
|
||||||
|
}, ensure_ascii=False),
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
records = generate_model_records(
|
||||||
|
[{"id": "preview-dpo", "edited_content": "扫描 PDF 缺少文本层时执行 OCR。"}],
|
||||||
|
model={"name": "model", "api_url": "https://model.example/v1"},
|
||||||
|
config={"output_type": "dpo", "generation_retries": 0},
|
||||||
|
task_id="task-dpo",
|
||||||
|
split={"train": 100, "validation": 0, "test": 0},
|
||||||
|
qa_pairs_per_item=1,
|
||||||
|
client=httpx.Client(transport=httpx.MockTransport(handler)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert records[0]["status"] == "valid"
|
||||||
|
assert records[0]["chosen"] == "仅在没有文本层时调用 OCR,并保留页码。"
|
||||||
|
assert records[0]["rejected"] == "所有 PDF 都重复执行 OCR。"
|
||||||
|
assert records[0]["output"] == records[0]["chosen"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_model_records_rejects_equal_dpo_pair() -> None:
|
||||||
|
def handler(_request: httpx.Request) -> httpx.Response:
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json={"choices": [{"message": {"content": json.dumps({
|
||||||
|
"items": [{
|
||||||
|
"instruction": "问题",
|
||||||
|
"chosen": "相同回答",
|
||||||
|
"rejected": "相同回答",
|
||||||
|
}],
|
||||||
|
}, ensure_ascii=False)}}]},
|
||||||
|
)
|
||||||
|
|
||||||
|
records = generate_model_records(
|
||||||
|
[{"id": "preview-dpo-invalid", "edited_content": "来源"}],
|
||||||
|
model={"name": "model", "api_url": "https://model.example/v1"},
|
||||||
|
config={"output_type": "dpo", "generation_retries": 0},
|
||||||
|
task_id="task-dpo-invalid",
|
||||||
|
split={"train": 100, "validation": 0, "test": 0},
|
||||||
|
qa_pairs_per_item=1,
|
||||||
|
client=httpx.Client(transport=httpx.MockTransport(handler)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert records[0]["status"] == "invalid"
|
||||||
|
assert "chosen equals rejected" in records[0]["error"]
|
||||||
|
|
||||||
|
|
||||||
def test_minimax_m3_uses_split_reasoning_and_completion_token_budget() -> None:
|
def test_minimax_m3_uses_split_reasoning_and_completion_token_budget() -> None:
|
||||||
requests: list[dict[str, object]] = []
|
requests: list[dict[str, object]] = []
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ def test_runtime_migration_fails_fast_on_incompatible_schema() -> None:
|
|||||||
for value in ("idle", "queued", "running", "completed", "failed", "cancelled"):
|
for value in ("idle", "queued", "running", "completed", "failed", "cancelled"):
|
||||||
assert f"'{value}'" in sql
|
assert f"'{value}'" in sql
|
||||||
assert "CREATE TABLE IF NOT EXISTS data_process_results" in sql
|
assert "CREATE TABLE IF NOT EXISTS data_process_results" in sql
|
||||||
|
assert "ADD COLUMN IF NOT EXISTS chosen TEXT NOT NULL DEFAULT ''" in sql
|
||||||
|
assert "ADD COLUMN IF NOT EXISTS rejected TEXT NOT NULL DEFAULT ''" in sql
|
||||||
|
assert "ADD COLUMN IF NOT EXISTS original_chosen TEXT" in sql
|
||||||
|
assert "ADD COLUMN IF NOT EXISTS original_rejected TEXT" in sql
|
||||||
assert sql.count("BEGIN;") == 1
|
assert sql.count("BEGIN;") == 1
|
||||||
assert sql.rstrip().endswith("COMMIT;")
|
assert sql.rstrip().endswith("COMMIT;")
|
||||||
|
|
||||||
|
|||||||
@@ -1335,6 +1335,39 @@ def test_publish_rejects_invalid_reasoning_output_format() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_publish_dpo_writes_chosen_and_rejected_jsonl() -> None:
|
||||||
|
conn = _PublishConnection(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "result-dpo",
|
||||||
|
"status": "valid",
|
||||||
|
"instruction": "如何处理扫描 PDF?",
|
||||||
|
"input": "",
|
||||||
|
"output": "仅在无文本层时执行 OCR。",
|
||||||
|
"chosen": "仅在无文本层时执行 OCR。",
|
||||||
|
"rejected": "所有 PDF 都执行 OCR。",
|
||||||
|
"preview_item_id": "preview-dpo",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
published = _PublishStore(conn, {"output_type": "dpo"}).publish(
|
||||||
|
"task-dpo",
|
||||||
|
{
|
||||||
|
"dataset_name": "偏好数据",
|
||||||
|
"storage_type": "local",
|
||||||
|
"format": "dpo",
|
||||||
|
"split": {"train": 100, "validation": 0, "test": 0},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
record = conn.records[0]["raw"]
|
||||||
|
assert record["chosen"] == "仅在无文本层时执行 OCR。"
|
||||||
|
assert record["rejected"] == "所有 PDF 都执行 OCR。"
|
||||||
|
assert "output" not in record
|
||||||
|
assert published["datasets"][0]["metadata"]["format"] == "dpo"
|
||||||
|
|
||||||
|
|
||||||
def test_source_storage_descriptor_accepts_owned_local_and_legacy_db_references() -> None:
|
def test_source_storage_descriptor_accepts_owned_local_and_legacy_db_references() -> None:
|
||||||
task_id = "dpt_task"
|
task_id = "dpt_task"
|
||||||
source_file_id = "dpsf_source"
|
source_file_id = "dpsf_source"
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ FROM python:3.12-slim
|
|||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PIP_NO_CACHE_DIR=1
|
PIP_NO_CACHE_DIR=1 \
|
||||||
|
TIKTOKEN_CACHE_DIR=/opt/tiktoken_cache
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -16,6 +17,9 @@ RUN python -c "import fastapi, uvicorn, psycopg, psycopg_pool, sqlalchemy, redis
|
|||||||
RUN mkdir -p /opt/yg-ft/logs/backend /data/yg-ft \
|
RUN mkdir -p /opt/yg-ft/logs/backend /data/yg-ft \
|
||||||
&& chmod -R 0775 /opt/yg-ft /data/yg-ft
|
&& chmod -R 0775 /opt/yg-ft /data/yg-ft
|
||||||
|
|
||||||
|
# 离线打包 tiktoken cl100k_base 词表,避免无网环境下运行时联网下载
|
||||||
|
COPY docker/app/tiktoken /opt/tiktoken_cache
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ assert.match(detailSource, /!\/\(\?:password\|secret\|token\|api_key\)\/i\.test\
|
|||||||
assert.match(detailSource, /key !== 'generation_model_snapshot'/, '处理配置仍直接展示内部模型快照')
|
assert.match(detailSource, /key !== 'generation_model_snapshot'/, '处理配置仍直接展示内部模型快照')
|
||||||
assert.match(detailSource, /preprocessOptionLabelMap/, '处理配置没有把预处理内部枚举转换为中文')
|
assert.match(detailSource, /preprocessOptionLabelMap/, '处理配置没有把预处理内部枚举转换为中文')
|
||||||
assert.match(detailSource, /output_type:\s*'输出类型'/, '处理配置没有显示输出类型名称')
|
assert.match(detailSource, /output_type:\s*'输出类型'/, '处理配置没有显示输出类型名称')
|
||||||
assert.match(detailSource, /value === 'reasoning' \? '思维链回答' : '标准回答'/, '处理配置没有转换输出类型枚举')
|
assert.match(detailSource, /value === 'reasoning' \? '思维链回答' : value === 'dpo' \? 'DPO 偏好对' : '标准回答'/, '处理配置没有转换输出类型枚举')
|
||||||
|
assert.match(detailSource, /prop="chosen"[\s\S]*?prop="rejected"/, 'DPO 结果没有展示 Chosen 与 Rejected')
|
||||||
|
assert.match(detailSource, /publishForm\.format = isDpoOutput\.value \? 'dpo'/, 'DPO 发布没有锁定原生格式')
|
||||||
assert.match(detailSource, /reasoning_detail:\s*'推理详细程度'/, '处理配置没有显示推理详细程度名称')
|
assert.match(detailSource, /reasoning_detail:\s*'推理详细程度'/, '处理配置没有显示推理详细程度名称')
|
||||||
assert.match(detailSource, /value === 'detailed' \? '详细推理' : '普通推理'/, '处理配置没有转换推理详细程度枚举')
|
assert.match(detailSource, /value === 'detailed' \? '详细推理' : '普通推理'/, '处理配置没有转换推理详细程度枚举')
|
||||||
assert.match(detailSource, /key !== 'reasoning_detail' \|\| config\.output_type === 'reasoning'/, '标准回答任务不应展示无关的推理详细程度')
|
assert.match(detailSource, /key !== 'reasoning_detail' \|\| config\.output_type === 'reasoning'/, '标准回答任务不应展示无关的推理详细程度')
|
||||||
|
|||||||
@@ -122,13 +122,13 @@ assert.match(regenerationSource, /route\.name === 'data-process-regenerate'[\s\S
|
|||||||
assert.match(viewSource, /const currentStep = ref\(0\)/, '重新生成必须从向导第一步开始')
|
assert.match(viewSource, /const currentStep = ref\(0\)/, '重新生成必须从向导第一步开始')
|
||||||
|
|
||||||
assert.match(viewSource, /const WIZARD_STEPS = \[/, '向导步骤尚未改为固定常量')
|
assert.match(viewSource, /const WIZARD_STEPS = \[/, '向导步骤尚未改为固定常量')
|
||||||
for (const title of ['创建任务', '大模型选择', '上传文件', '数据预览', '开始生成', '结果编辑与保存']) {
|
for (const title of ['创建任务', '大模型选择', '数据来源', '数据预览', '开始生成', '结果编辑与保存']) {
|
||||||
assert.ok(viewSource.includes(`title: '${title}'`), `缺少固定步骤:${title}`)
|
assert.ok(viewSource.includes(`title: '${title}'`), `缺少固定步骤:${title}`)
|
||||||
}
|
}
|
||||||
assert.match(
|
assert.match(
|
||||||
viewSource,
|
viewSource,
|
||||||
/\{ id: 'create',[\s\S]*?\{ id: 'model',[\s\S]*?\{ id: 'upload',[\s\S]*?\{ id: 'preview',[\s\S]*?\{ id: 'generate',[\s\S]*?\{ id: 'results'/,
|
/\{ id: 'create',[\s\S]*?\{ id: 'model',[\s\S]*?\{ id: 'upload',[\s\S]*?\{ id: 'preview',[\s\S]*?\{ id: 'generate',[\s\S]*?\{ id: 'results'/,
|
||||||
'六步向导顺序必须为创建任务、大模型选择、上传文件、数据预览、开始生成、结果编辑与保存',
|
'六步向导顺序必须为创建任务、大模型选择、数据来源、数据预览、开始生成、结果编辑与保存',
|
||||||
)
|
)
|
||||||
assert.doesNotMatch(viewSource, /steps\s*=\s*computed|all\.filter/, '步骤仍根据处理类型动态增减')
|
assert.doesNotMatch(viewSource, /steps\s*=\s*computed|all\.filter/, '步骤仍根据处理类型动态增减')
|
||||||
assert.match(
|
assert.match(
|
||||||
@@ -380,7 +380,7 @@ for (const marker of ['<el-upload', '源数据上传', '数据源配置', 'uploa
|
|||||||
assert.match(viewSource, /<ModelSelectionStep\s+[\s\S]*?v-else-if="currentStepId === 'model'"/, '第二步没有挂载独立大模型选择组件')
|
assert.match(viewSource, /<ModelSelectionStep\s+[\s\S]*?v-else-if="currentStepId === 'model'"/, '第二步没有挂载独立大模型选择组件')
|
||||||
assert.match(viewSource, /<SourceUploadStep\s+[\s\S]*?v-else-if="currentStepId === 'upload'"/, '第三步没有挂载独立上传组件')
|
assert.match(viewSource, /<SourceUploadStep\s+[\s\S]*?v-else-if="currentStepId === 'upload'"/, '第三步没有挂载独立上传组件')
|
||||||
assert.match(viewSource, /if \(currentStepId\.value === 'create'\) return '继续:选择大模型'/, '第一步主按钮没有指向大模型选择')
|
assert.match(viewSource, /if \(currentStepId\.value === 'create'\) return '继续:选择大模型'/, '第一步主按钮没有指向大模型选择')
|
||||||
assert.match(viewSource, /if \(currentStepId\.value === 'model'\) return '继续:上传文件'/, '第二步主按钮没有指向上传文件')
|
assert.match(viewSource, /if \(currentStepId\.value === 'model'\) return '继续:选择数据来源'/, '第二步主按钮没有指向数据来源选择')
|
||||||
assert.match(
|
assert.match(
|
||||||
viewSource,
|
viewSource,
|
||||||
/if \(currentStepId\.value === 'upload'\) \{[\s\S]*?sourceUploading\.value[\s\S]*?'正在上传'[\s\S]*?previewBuilding\.value \? '正在切分' : '继续:数据预览'/,
|
/if \(currentStepId\.value === 'upload'\) \{[\s\S]*?sourceUploading\.value[\s\S]*?'正在上传'[\s\S]*?previewBuilding\.value \? '正在切分' : '继续:数据预览'/,
|
||||||
@@ -1032,7 +1032,7 @@ assert.match(stateSource, /Object\.prototype\.hasOwnProperty\.call\(config, key\
|
|||||||
assert.match(stateSource, /Number\.isFinite\(value\) \? value : fallback/, '配置反向映射没有保留合法数字 0')
|
assert.match(stateSource, /Number\.isFinite\(value\) \? value : fallback/, '配置反向映射没有保留合法数字 0')
|
||||||
assert.match(stateSource, /qaPairsPerRow:\s*normalizeQaPairsGenerationCount\([\s\S]*?qa_pairs_per_row[\s\S]*?defaults\.qaPairsPerRow/, '结构化生成数量回填没有按 1 到 50 归一化')
|
assert.match(stateSource, /qaPairsPerRow:\s*normalizeQaPairsGenerationCount\([\s\S]*?qa_pairs_per_row[\s\S]*?defaults\.qaPairsPerRow/, '结构化生成数量回填没有按 1 到 50 归一化')
|
||||||
assert.match(stateSource, /qaPairsPerChunk:\s*normalizeQaPairsGenerationCount\([\s\S]*?qa_pairs_per_chunk[\s\S]*?defaults\.qaPairsPerChunk/, '非结构化生成数量回填没有按 1 到 50 归一化')
|
assert.match(stateSource, /qaPairsPerChunk:\s*normalizeQaPairsGenerationCount\([\s\S]*?qa_pairs_per_chunk[\s\S]*?defaults\.qaPairsPerChunk/, '非结构化生成数量回填没有按 1 到 50 归一化')
|
||||||
assert.match(stateSource, /const outputType = configValue\(config, 'output_type', defaults\.outputType\) === 'reasoning'[\s\S]*?\? 'reasoning'[\s\S]*?: 'standard'/, '输出类型没有从任务配置安全回填')
|
assert.match(stateSource, /configuredOutputType === 'reasoning' \|\| configuredOutputType === 'dpo'[\s\S]*?\? configuredOutputType[\s\S]*?: 'standard'/, '输出类型没有从任务配置安全回填')
|
||||||
assert.match(stateSource, /reasoningDetail:\s*configValue\(config, 'reasoning_detail', defaults\.reasoningDetail\) === 'detailed'[\s\S]*?\? 'detailed'[\s\S]*?: 'normal'/, '推理详细程度没有从任务配置安全回填')
|
assert.match(stateSource, /reasoningDetail:\s*configValue\(config, 'reasoning_detail', defaults\.reasoningDetail\) === 'detailed'[\s\S]*?\? 'detailed'[\s\S]*?: 'normal'/, '推理详细程度没有从任务配置安全回填')
|
||||||
assert.match(stateSource, /isBuiltInGenerationPrompt\(configuredPrompt\)[\s\S]*?defaultGenerationPrompt\(outputType\)/, '旧版内置提示语没有按输出类型迁移')
|
assert.match(stateSource, /isBuiltInGenerationPrompt\(configuredPrompt\)[\s\S]*?defaultGenerationPrompt\(outputType\)/, '旧版内置提示语没有按输出类型迁移')
|
||||||
assert.match(stateSource, /createStructuredOptionsFromConfig/, '结构化配置缺少后端到表单的反向映射')
|
assert.match(stateSource, /createStructuredOptionsFromConfig/, '结构化配置缺少后端到表单的反向映射')
|
||||||
@@ -1412,7 +1412,7 @@ assert.match(
|
|||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
sourceUploadSource,
|
sourceUploadSource,
|
||||||
/<el-upload\s+v-if="uploadedFiles\.length === 0"\s+drag\s+multiple\s+:accept="uploadAccept"\s+:disabled="previewBuilding"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"[^>]*>/,
|
/<el-upload\s+v-if="uploadedFiles\.length === 0"\s+drag\s+multiple\s+:accept="uploadAccept"\s+:disabled="previewBuilding \|\| externalPulling"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"[^>]*>/,
|
||||||
'无文件时未保留原有大拖拽上传区或上传配置',
|
'无文件时未保留原有大拖拽上传区或上传配置',
|
||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
@@ -1432,7 +1432,7 @@ assert.ok(sourceUploadSource.includes('旧版 DOC/PPT 请先转换'), '非结构
|
|||||||
assert.ok(sourceUploadSource.includes('旧版 XLS 请先转换'), '结构化格式提示没有说明旧版 XLS 需转换')
|
assert.ok(sourceUploadSource.includes('旧版 XLS 请先转换'), '结构化格式提示没有说明旧版 XLS 需转换')
|
||||||
assert.match(
|
assert.match(
|
||||||
sourceUploadSource,
|
sourceUploadSource,
|
||||||
/<div\s+class="uploaded-file-list-header">[\s\S]*?已选择 \{\{ uploadedFiles\.length \}\} 个文件[\s\S]*?<div\s+class="continue-upload">\s*<el-upload\s+multiple\s+:accept="uploadAccept"\s+:disabled="previewBuilding"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"[^>]*>\s*<el-button\s+size="small"\s+type="primary"\s+:disabled="previewBuilding">继续上传<\/el-button>\s*<\/el-upload>\s*<\/div>\s*<\/div>/,
|
/<div\s+class="uploaded-file-list-header">[\s\S]*?已选择 \{\{ uploadedFiles\.length \}\} 个文件[\s\S]*?<div\s+class="continue-upload">\s*<el-upload\s+multiple\s+:accept="uploadAccept"\s+:disabled="previewBuilding \|\| externalPulling"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"[^>]*>\s*<el-button\s+size="small"\s+type="primary"\s+:disabled="previewBuilding \|\| externalPulling">继续上传<\/el-button>\s*<\/el-upload>\s*<\/div>\s*<\/div>/,
|
||||||
'有文件时缺少标题右侧的继续上传触发器或上传配置',
|
'有文件时缺少标题右侧的继续上传触发器或上传配置',
|
||||||
)
|
)
|
||||||
assert.match(
|
assert.match(
|
||||||
@@ -1476,4 +1476,13 @@ assert.match(
|
|||||||
)
|
)
|
||||||
assert.match(previewSource, /height:\s*clamp\(560px,\s*calc\(100vh - 370px\),\s*720px\)/, '对照预览高度不足以展示切片正文')
|
assert.match(previewSource, /height:\s*clamp\(560px,\s*calc\(100vh - 370px\),\s*720px\)/, '对照预览高度不足以展示切片正文')
|
||||||
|
|
||||||
|
assert.doesNotMatch(taskSetupSource, /外来数据源拉取/, '外部数据源仍错误地放在第一步处理类型中')
|
||||||
|
assert.match(sourceUploadSource, /<h3>数据来源<\/h3>[\s\S]*?本地上传[\s\S]*?外部数据源/, '第三步缺少本地与外部数据来源选择')
|
||||||
|
for (const field of ['地址 / URL', '鉴权方式', 'SSL 模式', '连接超时', '查询超时', '只读查询语句', '拉取条数', '落地文件名']) {
|
||||||
|
assert.ok(sourceUploadSource.includes(field), `外部数据源标准配置缺少:${field}`)
|
||||||
|
}
|
||||||
|
assert.match(generationControlSource, /label="DPO 偏好对" value="dpo"/, '输出类型缺少 DPO 偏好对')
|
||||||
|
assert.match(resultEditorSource, /Chosen[\s\S]*?Rejected/, '结果编辑器缺少 DPO 成对字段')
|
||||||
|
assert.match(viewSource, /sourceConfigForBackend\(sourceMode\.value, externalSource\)/, '任务配置没有保存第三步数据来源模式')
|
||||||
|
|
||||||
console.log('数据处理六步向导回归检查通过')
|
console.log('数据处理六步向导回归检查通过')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export type DataProcessStatus = 'pending' | 'running' | 'completed' | 'failed' |
|
|||||||
export type DataProcessType = 'structured' | 'unstructured' | 'external'
|
export type DataProcessType = 'structured' | 'unstructured' | 'external'
|
||||||
export type DataProcessResultStatus = 'valid' | 'modified' | 'invalid'
|
export type DataProcessResultStatus = 'valid' | 'modified' | 'invalid'
|
||||||
export type DataProcessSplit = 'train' | 'validation' | 'test'
|
export type DataProcessSplit = 'train' | 'validation' | 'test'
|
||||||
export type DataProcessOutputType = 'standard' | 'reasoning'
|
export type DataProcessOutputType = 'standard' | 'reasoning' | 'dpo'
|
||||||
export type DataProcessReasoningDetail = 'normal' | 'detailed'
|
export type DataProcessReasoningDetail = 'normal' | 'detailed'
|
||||||
export type DataProcessWorkflowStep = 'create' | 'model' | 'upload' | 'preview' | 'generate' | 'results'
|
export type DataProcessWorkflowStep = 'create' | 'model' | 'upload' | 'preview' | 'generate' | 'results'
|
||||||
export type DataProcessPreviewStatus = 'idle' | 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
|
export type DataProcessPreviewStatus = 'idle' | 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
|
||||||
@@ -221,6 +221,9 @@ export interface DataProcessExternalSourcePayload {
|
|||||||
username?: string
|
username?: string
|
||||||
password?: string
|
password?: string
|
||||||
limit: number
|
limit: number
|
||||||
|
connect_timeout_seconds: number
|
||||||
|
statement_timeout_seconds: number
|
||||||
|
ssl_mode: 'disable' | 'prefer' | 'require' | 'verify-ca' | 'verify-full'
|
||||||
query?: string
|
query?: string
|
||||||
file_name?: string
|
file_name?: string
|
||||||
}
|
}
|
||||||
@@ -342,9 +345,13 @@ export interface DataProcessResult {
|
|||||||
instruction: string
|
instruction: string
|
||||||
input: string
|
input: string
|
||||||
output: string
|
output: string
|
||||||
|
chosen?: string
|
||||||
|
rejected?: string
|
||||||
original_instruction?: string | null
|
original_instruction?: string | null
|
||||||
original_input?: string | null
|
original_input?: string | null
|
||||||
original_output?: string | null
|
original_output?: string | null
|
||||||
|
original_chosen?: string | null
|
||||||
|
original_rejected?: string | null
|
||||||
status: DataProcessResultStatus
|
status: DataProcessResultStatus
|
||||||
error?: string | null
|
error?: string | null
|
||||||
split?: DataProcessSplit | null
|
split?: DataProcessSplit | null
|
||||||
@@ -356,6 +363,8 @@ export interface DataProcessResultUpdatePayload {
|
|||||||
instruction: string
|
instruction: string
|
||||||
input: string
|
input: string
|
||||||
output: string
|
output: string
|
||||||
|
chosen?: string
|
||||||
|
rejected?: string
|
||||||
expected_updated_at?: string
|
expected_updated_at?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +419,7 @@ export interface DataProcessPublishPayload {
|
|||||||
dataset_type: 'train' | 'test' | 'eval' | 'val' | 'other'
|
dataset_type: 'train' | 'test' | 'eval' | 'val' | 'other'
|
||||||
storage_type: 'local'
|
storage_type: 'local'
|
||||||
split: DataProcessDatasetSplit
|
split: DataProcessDatasetSplit
|
||||||
format: 'alpaca_jsonl' | 'jsonl'
|
format: 'alpaca_jsonl' | 'jsonl' | 'dpo'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataProcessPublishResult {
|
export interface DataProcessPublishResult {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { useDataProcessGeneration } from './create/useDataProcessGeneration'
|
import { useDataProcessGeneration } from './create/useDataProcessGeneration'
|
||||||
import { useDataProcessPreviewBuild } from './create/useDataProcessPreviewBuild'
|
import { useDataProcessPreviewBuild } from './create/useDataProcessPreviewBuild'
|
||||||
import { useDataProcessRegeneration } from './create/useDataProcessRegeneration'
|
import { useDataProcessRegeneration } from './create/useDataProcessRegeneration'
|
||||||
|
import { createDefaultExternalSource, externalSourcePayload, restoreExternalSourceConfig, sourceConfigForBackend } from './create/externalSourceConfig'
|
||||||
import {
|
import {
|
||||||
loadCanonicalSourceContent,
|
loadCanonicalSourceContent,
|
||||||
mapDataProcessSourceFile,
|
mapDataProcessSourceFile,
|
||||||
@@ -39,26 +40,20 @@ import {
|
|||||||
updateDataProcessPreview,
|
updateDataProcessPreview,
|
||||||
updateDataProcessTask,
|
updateDataProcessTask,
|
||||||
updateDataProcessWorkflowStep,
|
updateDataProcessWorkflowStep,
|
||||||
type DataProcessExternalSourcePayload,
|
|
||||||
type DataProcessPreviewItem,
|
type DataProcessPreviewItem,
|
||||||
} from '@/api/modules/dataProcess'
|
} from '@/api/modules/dataProcess'
|
||||||
import type {
|
import type { DataProcessConfig, DataProcessPreviewProgress, DataProcessTask, DataProcessWorkflowStep } from '@/types/dataProcess'
|
||||||
DataProcessConfig,
|
|
||||||
DataProcessPreviewProgress,
|
|
||||||
DataProcessTask,
|
|
||||||
DataProcessWorkflowStep,
|
|
||||||
} from '@/types/dataProcess'
|
|
||||||
import type {
|
import type {
|
||||||
ExternalDataSource,
|
ExternalDataSource,
|
||||||
GenerationControlOptions,
|
GenerationControlOptions,
|
||||||
PreviewItem,
|
PreviewItem,
|
||||||
ProcessType,
|
ProcessType,
|
||||||
|
SourceMode,
|
||||||
StepId,
|
StepId,
|
||||||
StructuredProcessOptions,
|
StructuredProcessOptions,
|
||||||
UnstructuredProcessOptions,
|
UnstructuredProcessOptions,
|
||||||
UploadedDataFile,
|
UploadedDataFile,
|
||||||
} from './create/types'
|
} from './create/types'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const modelsStore = useModelsStore()
|
const modelsStore = useModelsStore()
|
||||||
const { list: modelList, loaded: modelsLoaded } = storeToRefs(modelsStore)
|
const { list: modelList, loaded: modelsLoaded } = storeToRefs(modelsStore)
|
||||||
@@ -71,7 +66,7 @@ const PREVIEW_MODEL_VERSION = 'backend-pipeline-v4'
|
|||||||
const WIZARD_STEPS = [
|
const WIZARD_STEPS = [
|
||||||
{ id: 'create', title: '创建任务', desc: '填写任务信息与处理配置' },
|
{ id: 'create', title: '创建任务', desc: '填写任务信息与处理配置' },
|
||||||
{ id: 'model', title: '大模型选择', desc: '选择生成模型并设置输出要求' },
|
{ id: 'model', title: '大模型选择', desc: '选择生成模型并设置输出要求' },
|
||||||
{ id: 'upload', title: '上传文件', desc: '上传或接入待处理的源数据' },
|
{ id: 'upload', title: '数据来源', desc: '选择本地上传或外部数据源拉取' },
|
||||||
{ id: 'preview', title: '数据预览', desc: '核对源文件与预览内容' },
|
{ id: 'preview', title: '数据预览', desc: '核对源文件与预览内容' },
|
||||||
{ id: 'generate', title: '开始生成', desc: '确认摘要并启动处理' },
|
{ id: 'generate', title: '开始生成', desc: '确认摘要并启动处理' },
|
||||||
{ id: 'results', title: '结果编辑与保存', desc: '检查、修改并保存结果' },
|
{ id: 'results', title: '结果编辑与保存', desc: '检查、修改并保存结果' },
|
||||||
@@ -81,11 +76,13 @@ const currentStepId = computed<StepId>(() => WIZARD_STEPS[currentStep.value]?.id
|
|||||||
const task = reactive({ name: '', description: '' })
|
const task = reactive({ name: '', description: '' })
|
||||||
const taskId = ref<string | null>(null)
|
const taskId = ref<string | null>(null)
|
||||||
const processType = ref<ProcessType>('structured')
|
const processType = ref<ProcessType>('structured')
|
||||||
|
const sourceMode = ref<SourceMode>('local')
|
||||||
const structuredOptions = ref<StructuredProcessOptions>(createDefaultStructuredOptions())
|
const structuredOptions = ref<StructuredProcessOptions>(createDefaultStructuredOptions())
|
||||||
const unstructuredOptions = ref<UnstructuredProcessOptions>(createDefaultUnstructuredOptions())
|
const unstructuredOptions = ref<UnstructuredProcessOptions>(createDefaultUnstructuredOptions())
|
||||||
const modelSelectionOptions = computed<GenerationControlOptions>(() => (
|
const modelSelectionOptions = computed<GenerationControlOptions>(() => (
|
||||||
processType.value === 'unstructured' ? unstructuredOptions.value : structuredOptions.value
|
processType.value === 'unstructured' ? unstructuredOptions.value : structuredOptions.value
|
||||||
))
|
))
|
||||||
|
const activeOutputType = computed(() => modelSelectionOptions.value.outputType)
|
||||||
const uploadedFiles = ref<UploadedDataFile[]>([])
|
const uploadedFiles = ref<UploadedDataFile[]>([])
|
||||||
const previewBuilding = ref(false)
|
const previewBuilding = ref(false)
|
||||||
const {
|
const {
|
||||||
@@ -93,16 +90,7 @@ const {
|
|||||||
startPreviewBuild,
|
startPreviewBuild,
|
||||||
stopPreviewPolling,
|
stopPreviewPolling,
|
||||||
} = useDataProcessPreviewBuild()
|
} = useDataProcessPreviewBuild()
|
||||||
const externalSource = reactive<ExternalDataSource>({
|
const externalSource = reactive<ExternalDataSource>(createDefaultExternalSource())
|
||||||
type: 'postgresql',
|
|
||||||
url: '',
|
|
||||||
authMode: 'none',
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
limit: 1000,
|
|
||||||
query: '',
|
|
||||||
fileName: 'external-data.jsonl',
|
|
||||||
})
|
|
||||||
const externalPulling = ref(false)
|
const externalPulling = ref(false)
|
||||||
const externalConnected = ref(false)
|
const externalConnected = ref(false)
|
||||||
const fileName = computed(() => uploadedFiles.value.map(f => f.name).join(', '))
|
const fileName = computed(() => uploadedFiles.value.map(f => f.name).join(', '))
|
||||||
@@ -135,6 +123,7 @@ const {
|
|||||||
} = useDataProcessGeneration({
|
} = useDataProcessGeneration({
|
||||||
taskId,
|
taskId,
|
||||||
dirty,
|
dirty,
|
||||||
|
outputType: activeOutputType,
|
||||||
beforeGenerate: beforeStartGeneration,
|
beforeGenerate: beforeStartGeneration,
|
||||||
})
|
})
|
||||||
const { enqueueSourceUpload, sourceUploading } = useDataProcessSourceUpload({
|
const { enqueueSourceUpload, sourceUploading } = useDataProcessSourceUpload({
|
||||||
@@ -174,7 +163,7 @@ const previewFiles = computed(() => uploadedFiles.value.map((file) => {
|
|||||||
}))
|
}))
|
||||||
const primaryActionLabel = computed(() => {
|
const primaryActionLabel = computed(() => {
|
||||||
if (currentStepId.value === 'create') return '继续:选择大模型'
|
if (currentStepId.value === 'create') return '继续:选择大模型'
|
||||||
if (currentStepId.value === 'model') return '继续:上传文件'
|
if (currentStepId.value === 'model') return '继续:选择数据来源'
|
||||||
if (currentStepId.value === 'upload') {
|
if (currentStepId.value === 'upload') {
|
||||||
if (sourceUploading.value) return '正在上传'
|
if (sourceUploading.value) return '正在上传'
|
||||||
return previewBuilding.value ? '正在切分' : '继续:数据预览'
|
return previewBuilding.value ? '正在切分' : '继续:数据预览'
|
||||||
@@ -198,7 +187,6 @@ function goToStep(stepId: StepId) {
|
|||||||
const nextStepIndex = WIZARD_STEPS.findIndex((step) => step.id === stepId)
|
const nextStepIndex = WIZARD_STEPS.findIndex((step) => step.id === stepId)
|
||||||
if (nextStepIndex >= 0) currentStep.value = nextStepIndex
|
if (nextStepIndex >= 0) currentStep.value = nextStepIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateModelSelectionOptions(value: GenerationControlOptions) {
|
function updateModelSelectionOptions(value: GenerationControlOptions) {
|
||||||
if (processType.value === 'unstructured') {
|
if (processType.value === 'unstructured') {
|
||||||
unstructuredOptions.value = { ...unstructuredOptions.value, ...value }
|
unstructuredOptions.value = { ...unstructuredOptions.value, ...value }
|
||||||
@@ -206,12 +194,12 @@ function updateModelSelectionOptions(value: GenerationControlOptions) {
|
|||||||
}
|
}
|
||||||
structuredOptions.value = { ...structuredOptions.value, ...value }
|
structuredOptions.value = { ...structuredOptions.value, ...value }
|
||||||
}
|
}
|
||||||
|
|
||||||
function toBackendConfig(): DataProcessConfig {
|
function toBackendConfig(): DataProcessConfig {
|
||||||
const options = processType.value === 'unstructured'
|
const options = processType.value === 'unstructured'
|
||||||
? unstructuredOptions.value
|
? unstructuredOptions.value
|
||||||
: structuredOptions.value
|
: structuredOptions.value
|
||||||
const common = {
|
const common = {
|
||||||
|
...sourceConfigForBackend(sourceMode.value, externalSource),
|
||||||
preprocess_options: [...options.preprocessOptions],
|
preprocess_options: [...options.preprocessOptions],
|
||||||
semantic_enrichment: options.semanticEnrichment,
|
semantic_enrichment: options.semanticEnrichment,
|
||||||
dataset_split: { ...options.datasetSplit },
|
dataset_split: { ...options.datasetSplit },
|
||||||
@@ -247,7 +235,6 @@ function toBackendConfig(): DataProcessConfig {
|
|||||||
qa_pairs_per_row: structuredOptions.value.qaPairsPerRow,
|
qa_pairs_per_row: structuredOptions.value.qaPairsPerRow,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function taskPayload() {
|
function taskPayload() {
|
||||||
return {
|
return {
|
||||||
name: task.name.trim(),
|
name: task.name.trim(),
|
||||||
@@ -256,12 +243,10 @@ function taskPayload() {
|
|||||||
config: toBackendConfig(),
|
config: toBackendConfig(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function persistWorkflowStep(step: StepId) {
|
async function persistWorkflowStep(step: StepId) {
|
||||||
if (!taskId.value) return
|
if (!taskId.value) return
|
||||||
await updateDataProcessWorkflowStep(taskId.value, step as DataProcessWorkflowStep)
|
await updateDataProcessWorkflowStep(taskId.value, step as DataProcessWorkflowStep)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveTaskConfiguration() {
|
async function saveTaskConfiguration() {
|
||||||
if (isRegeneration.value) {
|
if (isRegeneration.value) {
|
||||||
const regenerated = await prepareRegeneration(taskPayload())
|
const regenerated = await prepareRegeneration(taskPayload())
|
||||||
@@ -277,19 +262,6 @@ async function saveTaskConfiguration() {
|
|||||||
return saved
|
return saved
|
||||||
}
|
}
|
||||||
|
|
||||||
function externalPayload(): DataProcessExternalSourcePayload {
|
|
||||||
return {
|
|
||||||
type: 'postgresql',
|
|
||||||
url: externalSource.url.trim(),
|
|
||||||
auth_mode: externalSource.authMode,
|
|
||||||
username: externalSource.username || undefined,
|
|
||||||
password: externalSource.password || undefined,
|
|
||||||
limit: externalSource.limit,
|
|
||||||
query: externalSource.query?.trim() || undefined,
|
|
||||||
file_name: externalSource.fileName || 'external-data.jsonl',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapPreviewItem(item: DataProcessPreviewItem): PreviewItem {
|
function mapPreviewItem(item: DataProcessPreviewItem): PreviewItem {
|
||||||
const sourceLocator = item.quality_score?.source_locator
|
const sourceLocator = item.quality_score?.source_locator
|
||||||
return {
|
return {
|
||||||
@@ -378,7 +350,7 @@ async function beforeStartGeneration() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
watch(
|
watch(
|
||||||
[() => task.name, () => task.description, processType, structuredOptions, unstructuredOptions, externalSource],
|
[() => task.name, () => task.description, processType, sourceMode, structuredOptions, unstructuredOptions, externalSource],
|
||||||
() => {
|
() => {
|
||||||
if (!hydrating.value) dirty.value = true
|
if (!hydrating.value) dirty.value = true
|
||||||
},
|
},
|
||||||
@@ -396,6 +368,13 @@ watch(processType, (nextType, previousType) => {
|
|||||||
ElMessage.info('处理类型已变更,请重新上传或拉取匹配的源数据')
|
ElMessage.info('处理类型已变更,请重新上传或拉取匹配的源数据')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(processType, (nextType) => {
|
||||||
|
if (nextType === 'unstructured' && sourceMode.value === 'external') {
|
||||||
|
sourceMode.value = 'local'
|
||||||
|
externalConnected.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
watch(generationOptionsSignature, (currentSignature, previousSignature) => {
|
watch(generationOptionsSignature, (currentSignature, previousSignature) => {
|
||||||
if (hydrating.value) return
|
if (hydrating.value) return
|
||||||
if (currentSignature === previousSignature) return
|
if (currentSignature === previousSignature) return
|
||||||
@@ -453,6 +432,16 @@ function updateExternalSource(value: ExternalDataSource) {
|
|||||||
externalConnected.value = false
|
externalConnected.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateSourceMode(value: SourceMode) {
|
||||||
|
const previous = sourceMode.value
|
||||||
|
sourceMode.value = value
|
||||||
|
externalConnected.value = false
|
||||||
|
dirty.value = true
|
||||||
|
externalPulling.value = true
|
||||||
|
try { await saveTaskConfiguration() } catch { sourceMode.value = previous }
|
||||||
|
finally { externalPulling.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
async function handleTestConnection() {
|
async function handleTestConnection() {
|
||||||
if (!taskId.value) {
|
if (!taskId.value) {
|
||||||
ElMessage.error('任务尚未创建,请返回模型选择步骤后重试')
|
ElMessage.error('任务尚未创建,请返回模型选择步骤后重试')
|
||||||
@@ -464,7 +453,8 @@ async function handleTestConnection() {
|
|||||||
}
|
}
|
||||||
externalPulling.value = true
|
externalPulling.value = true
|
||||||
try {
|
try {
|
||||||
const result = await testDataProcessExternalSource(taskId.value, externalPayload())
|
await saveTaskConfiguration()
|
||||||
|
const result = await testDataProcessExternalSource(taskId.value, externalSourcePayload(externalSource))
|
||||||
externalConnected.value = result.connected
|
externalConnected.value = result.connected
|
||||||
if (result.connected) ElMessage.success(result.message || '数据源连接测试成功')
|
if (result.connected) ElMessage.success(result.message || '数据源连接测试成功')
|
||||||
else ElMessage.warning(result.message || '数据源连接失败')
|
else ElMessage.warning(result.message || '数据源连接失败')
|
||||||
@@ -490,7 +480,8 @@ async function handlePullData() {
|
|||||||
}
|
}
|
||||||
externalPulling.value = true
|
externalPulling.value = true
|
||||||
try {
|
try {
|
||||||
const response = await pullDataProcessExternalSource(taskId.value, externalPayload())
|
await saveTaskConfiguration()
|
||||||
|
const response = await pullDataProcessExternalSource(taskId.value, externalSourcePayload(externalSource))
|
||||||
const newFiles: UploadedDataFile[] = []
|
const newFiles: UploadedDataFile[] = []
|
||||||
for (const file of response.files) {
|
for (const file of response.files) {
|
||||||
const content = await loadCanonicalSourceContent(taskId.value, file.id)
|
const content = await loadCanonicalSourceContent(taskId.value, file.id)
|
||||||
@@ -620,6 +611,12 @@ function applyPreviewProgress(progress: DataProcessPreviewProgress) {
|
|||||||
|
|
||||||
async function completePreviewWorkspace() {
|
async function completePreviewWorkspace() {
|
||||||
previewItems.value = await loadAllPreviewItems()
|
previewItems.value = await loadAllPreviewItems()
|
||||||
|
for (const file of uploadedFiles.value) {
|
||||||
|
if (!file.content && file.sourceFileId && file.fileFormat?.replace('.', '') === 'pdf') {
|
||||||
|
file.content = await loadCanonicalSourceContent(taskId.value as string, file.sourceFileId)
|
||||||
|
.catch(() => file.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
const configSignature = buildPreviewConfigSignature()
|
const configSignature = buildPreviewConfigSignature()
|
||||||
const previewCounts = new Map<string, number>()
|
const previewCounts = new Map<string, number>()
|
||||||
for (const item of previewItems.value) {
|
for (const item of previewItems.value) {
|
||||||
@@ -678,7 +675,7 @@ async function nextFromUpload() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (uploadedFiles.value.length === 0) {
|
if (uploadedFiles.value.length === 0) {
|
||||||
ElMessage.warning(processType.value === 'external' ? '请先拉取至少一个数据源' : '请上传至少一个源数据文件')
|
ElMessage.warning(sourceMode.value === 'external' ? '请先拉取至少一个数据源' : '请上传至少一个源数据文件')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const failedUploads = uploadedFiles.value.filter((file) => file.status === 'failed')
|
const failedUploads = uploadedFiles.value.filter((file) => file.status === 'failed')
|
||||||
@@ -992,6 +989,9 @@ async function initializeExistingWorkflow() {
|
|||||||
const sourceTask = await loadRegenerationSource()
|
const sourceTask = await loadRegenerationSource()
|
||||||
if (!sourceTask) return
|
if (!sourceTask) return
|
||||||
taskId.value = String(sourceTask.id)
|
taskId.value = String(sourceTask.id)
|
||||||
|
const restoredSource = restoreExternalSourceConfig(sourceTask.config || {})
|
||||||
|
sourceMode.value = restoredSource.mode
|
||||||
|
Object.assign(externalSource, restoredSource.source)
|
||||||
if (!isWorkflowResume.value) return
|
if (!isWorkflowResume.value) return
|
||||||
if (sourceTask.status === 'completed' && sourceTask.results_confirmed !== false) {
|
if (sourceTask.status === 'completed' && sourceTask.results_confirmed !== false) {
|
||||||
allowLeave = true
|
allowLeave = true
|
||||||
@@ -1102,6 +1102,7 @@ onMounted(() => {
|
|||||||
<SourceUploadStep
|
<SourceUploadStep
|
||||||
v-else-if="currentStepId === 'upload'"
|
v-else-if="currentStepId === 'upload'"
|
||||||
:process-type="processType"
|
:process-type="processType"
|
||||||
|
:source-mode="sourceMode"
|
||||||
:uploaded-files="uploadedFiles"
|
:uploaded-files="uploadedFiles"
|
||||||
:external-source="externalSource"
|
:external-source="externalSource"
|
||||||
:external-pulling="externalPulling"
|
:external-pulling="externalPulling"
|
||||||
@@ -1109,6 +1110,7 @@ onMounted(() => {
|
|||||||
:preview-building="previewBuilding"
|
:preview-building="previewBuilding"
|
||||||
:source-uploading="sourceUploading"
|
:source-uploading="sourceUploading"
|
||||||
@update:external-source="updateExternalSource"
|
@update:external-source="updateExternalSource"
|
||||||
|
@update:source-mode="updateSourceMode"
|
||||||
@file-change="handleFileChange"
|
@file-change="handleFileChange"
|
||||||
@remove-file="handleRemoveFile"
|
@remove-file="handleRemoveFile"
|
||||||
@use-sample="useSampleFile"
|
@use-sample="useSampleFile"
|
||||||
@@ -1154,6 +1156,7 @@ onMounted(() => {
|
|||||||
:preview-items="previewItems"
|
:preview-items="previewItems"
|
||||||
:regenerating-result-id="regeneratingResultId"
|
:regenerating-result-id="regeneratingResultId"
|
||||||
:bulk-regeneration="bulkRegeneration"
|
:bulk-regeneration="bulkRegeneration"
|
||||||
|
:output-type="activeOutputType"
|
||||||
@update:field="updateResultField"
|
@update:field="updateResultField"
|
||||||
@regenerate:all="regenerateAllResults"
|
@regenerate:all="regenerateAllResults"
|
||||||
@regenerate:item="regenerateResult"
|
@regenerate:item="regenerateResult"
|
||||||
@@ -1179,8 +1182,8 @@ onMounted(() => {
|
|||||||
<el-button
|
<el-button
|
||||||
class="wizard-primary-action"
|
class="wizard-primary-action"
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="modelSubmitLoading || generationStarting || generation.status === 'running' || resultRegenerationBusy || (currentStepId === 'upload' && (sourceUploading || previewBuilding))"
|
:loading="externalPulling || modelSubmitLoading || generationStarting || generation.status === 'running' || resultRegenerationBusy || (currentStepId === 'upload' && (sourceUploading || previewBuilding))"
|
||||||
:disabled="hydrating || modelSubmitLoading || generationStarting || Boolean(initializationError) || resultRegenerationBusy || (currentStepId === 'generate' && generation.status === 'running') || previewBuilding || sourceUploading || (currentStepId === 'upload' && hasUnfinishedUploads)"
|
:disabled="externalPulling || hydrating || modelSubmitLoading || generationStarting || Boolean(initializationError) || resultRegenerationBusy || (currentStepId === 'generate' && generation.status === 'running') || previewBuilding || sourceUploading || (currentStepId === 'upload' && hasUnfinishedUploads)"
|
||||||
@click="handlePrimaryAction"
|
@click="handlePrimaryAction"
|
||||||
>
|
>
|
||||||
{{ primaryActionLabel }} <i class="fa" :class="primaryActionIcon" style="margin-left: 6px;" />
|
{{ primaryActionLabel }} <i class="fa" :class="primaryActionIcon" style="margin-left: 6px;" />
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const resultCellTooltipOptions = {
|
|||||||
} as const
|
} as const
|
||||||
let resultFilterTimer: ReturnType<typeof setTimeout> | null = null
|
let resultFilterTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
const editForm = reactive({ instruction: '', input: '', output: '' })
|
const editForm = reactive({ instruction: '', input: '', output: '', chosen: '', rejected: '' })
|
||||||
const publishForm = reactive<DataProcessPublishPayload>({
|
const publishForm = reactive<DataProcessPublishPayload>({
|
||||||
dataset_name: '',
|
dataset_name: '',
|
||||||
dataset_type: 'train',
|
dataset_type: 'train',
|
||||||
@@ -69,6 +69,8 @@ const processTypeMap: Record<DataProcessType, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const configLabelMap: Record<string, string> = {
|
const configLabelMap: Record<string, string> = {
|
||||||
|
source_mode: '数据来源',
|
||||||
|
external_source: '外部数据源配置',
|
||||||
preprocess_options: '预处理规则',
|
preprocess_options: '预处理规则',
|
||||||
dataset_split: '数据集划分',
|
dataset_split: '数据集划分',
|
||||||
generation_model_id: '数据生成模型',
|
generation_model_id: '数据生成模型',
|
||||||
@@ -212,6 +214,7 @@ const outputDatasetName = computed(() => (
|
|||||||
))
|
))
|
||||||
const outputDatasetId = computed(() => detail.value?.output_dataset_id || null)
|
const outputDatasetId = computed(() => detail.value?.output_dataset_id || null)
|
||||||
const hasPublishedOutputs = computed(() => outputDatasets.value.length > 0)
|
const hasPublishedOutputs = computed(() => outputDatasets.value.length > 0)
|
||||||
|
const isDpoOutput = computed(() => detail.value?.config?.output_type === 'dpo')
|
||||||
const hasCurrentPublishedDataset = computed(() => (
|
const hasCurrentPublishedDataset = computed(() => (
|
||||||
Boolean(outputDatasetId.value)
|
Boolean(outputDatasetId.value)
|
||||||
&& outputDatasets.value.some((dataset) => String(dataset.id) === String(outputDatasetId.value))
|
&& outputDatasets.value.some((dataset) => String(dataset.id) === String(outputDatasetId.value))
|
||||||
@@ -294,6 +297,18 @@ const configRows = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function formatConfigValue(key: string, value: unknown) {
|
function formatConfigValue(key: string, value: unknown) {
|
||||||
|
if (key === 'source_mode') return value === 'external' ? '外部数据源' : '本地上传'
|
||||||
|
if (key === 'external_source' && value && typeof value === 'object') {
|
||||||
|
const source = value as Record<string, unknown>
|
||||||
|
return [
|
||||||
|
source.type,
|
||||||
|
source.url,
|
||||||
|
source.ssl_mode ? `SSL ${source.ssl_mode}` : '',
|
||||||
|
source.limit ? `上限 ${source.limit} 条` : '',
|
||||||
|
source.connect_timeout_seconds ? `连接 ${source.connect_timeout_seconds}s` : '',
|
||||||
|
source.statement_timeout_seconds ? `查询 ${source.statement_timeout_seconds}s` : '',
|
||||||
|
].filter(Boolean).join(' · ')
|
||||||
|
}
|
||||||
if (key === 'generation_model_id') {
|
if (key === 'generation_model_id') {
|
||||||
const snapshot = detail.value?.config?.generation_model_snapshot
|
const snapshot = detail.value?.config?.generation_model_snapshot
|
||||||
if (snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot)) {
|
if (snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot)) {
|
||||||
@@ -305,7 +320,7 @@ function formatConfigValue(key: string, value: unknown) {
|
|||||||
return chunkMethodLabelMap[value] || value
|
return chunkMethodLabelMap[value] || value
|
||||||
}
|
}
|
||||||
if (key === 'output_type') {
|
if (key === 'output_type') {
|
||||||
return value === 'reasoning' ? '思维链回答' : '标准回答'
|
return value === 'reasoning' ? '思维链回答' : value === 'dpo' ? 'DPO 偏好对' : '标准回答'
|
||||||
}
|
}
|
||||||
if (key === 'reasoning_detail') {
|
if (key === 'reasoning_detail') {
|
||||||
return value === 'detailed' ? '详细推理' : '普通推理'
|
return value === 'detailed' ? '详细推理' : '普通推理'
|
||||||
@@ -461,14 +476,26 @@ function openResultEditor(result: DataProcessResult) {
|
|||||||
instruction: result.instruction,
|
instruction: result.instruction,
|
||||||
input: result.input,
|
input: result.input,
|
||||||
output: result.output,
|
output: result.output,
|
||||||
|
chosen: result.chosen || result.output || '',
|
||||||
|
rejected: result.rejected || '',
|
||||||
})
|
})
|
||||||
editDialogVisible.value = true
|
editDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveResult() {
|
async function saveResult() {
|
||||||
if (!editingResult.value) return
|
if (!editingResult.value) return
|
||||||
if (!editForm.instruction.trim() || !editForm.output.trim()) {
|
const invalid = isDpoOutput.value
|
||||||
ElMessage.warning('Instruction 和 Output 不能为空')
|
? !editForm.instruction.trim()
|
||||||
|
|| !editForm.chosen.trim()
|
||||||
|
|| !editForm.rejected.trim()
|
||||||
|
|| editForm.chosen.trim() === editForm.rejected.trim()
|
||||||
|
: !editForm.instruction.trim() || !editForm.output.trim()
|
||||||
|
if (invalid) {
|
||||||
|
ElMessage.warning(
|
||||||
|
isDpoOutput.value
|
||||||
|
? 'Instruction、Chosen、Rejected 均不能为空,且两个回答不能相同'
|
||||||
|
: 'Instruction 和 Output 不能为空',
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,6 +505,8 @@ async function saveResult() {
|
|||||||
instruction: editForm.instruction,
|
instruction: editForm.instruction,
|
||||||
input: editForm.input,
|
input: editForm.input,
|
||||||
output: editForm.output,
|
output: editForm.output,
|
||||||
|
chosen: editForm.chosen,
|
||||||
|
rejected: editForm.rejected,
|
||||||
expected_updated_at: editingResult.value.updated_at,
|
expected_updated_at: editingResult.value.updated_at,
|
||||||
})
|
})
|
||||||
replaceResult(updated)
|
replaceResult(updated)
|
||||||
@@ -530,6 +559,7 @@ function openPublishDialog() {
|
|||||||
if (!detail.value) return
|
if (!detail.value) return
|
||||||
publishForm.dataset_name = `${detail.value.name}-数据集`
|
publishForm.dataset_name = `${detail.value.name}-数据集`
|
||||||
publishForm.split = configuredSplit()
|
publishForm.split = configuredSplit()
|
||||||
|
publishForm.format = isDpoOutput.value ? 'dpo' : 'alpaca_jsonl'
|
||||||
publishDialogVisible.value = true
|
publishDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,7 +838,11 @@ onBeforeUnmount(() => {
|
|||||||
<el-table-column type="index" label="#" width="56" align="center" />
|
<el-table-column type="index" label="#" width="56" align="center" />
|
||||||
<el-table-column label="指令" min-width="190" show-overflow-tooltip prop="instruction" />
|
<el-table-column label="指令" min-width="190" show-overflow-tooltip prop="instruction" />
|
||||||
<el-table-column label="输入" min-width="160" show-overflow-tooltip prop="input" />
|
<el-table-column label="输入" min-width="160" show-overflow-tooltip prop="input" />
|
||||||
<el-table-column label="输出" min-width="230" show-overflow-tooltip prop="output" />
|
<el-table-column v-if="!isDpoOutput" label="输出" min-width="230" show-overflow-tooltip prop="output" />
|
||||||
|
<template v-else>
|
||||||
|
<el-table-column label="Chosen" min-width="210" show-overflow-tooltip prop="chosen" />
|
||||||
|
<el-table-column label="Rejected" min-width="210" show-overflow-tooltip prop="rejected" />
|
||||||
|
</template>
|
||||||
<el-table-column label="质量分" width="88" align="center">
|
<el-table-column label="质量分" width="88" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tooltip :content="qualityFlagsLabel((row as DataProcessResult).quality_score)">
|
<el-tooltip :content="qualityFlagsLabel((row as DataProcessResult).quality_score)">
|
||||||
@@ -862,9 +896,17 @@ onBeforeUnmount(() => {
|
|||||||
<el-form-item label="Input">
|
<el-form-item label="Input">
|
||||||
<el-input v-model="editForm.input" type="textarea" :rows="3" maxlength="10000" show-word-limit />
|
<el-input v-model="editForm.input" type="textarea" :rows="3" maxlength="10000" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Output" required>
|
<el-form-item v-if="!isDpoOutput" label="Output" required>
|
||||||
<el-input v-model="editForm.output" type="textarea" :rows="6" maxlength="20000" show-word-limit />
|
<el-input v-model="editForm.output" type="textarea" :rows="6" maxlength="20000" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<template v-else>
|
||||||
|
<el-form-item label="Chosen(优选回答)" required>
|
||||||
|
<el-input v-model="editForm.chosen" type="textarea" :rows="6" maxlength="20000" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Rejected(拒选回答)" required>
|
||||||
|
<el-input v-model="editForm.rejected" type="textarea" :rows="6" maxlength="20000" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||||
@@ -884,7 +926,8 @@ onBeforeUnmount(() => {
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="输出格式">
|
<el-form-item label="输出格式">
|
||||||
<el-select v-model="publishForm.format">
|
<el-select v-model="publishForm.format" :disabled="isDpoOutput">
|
||||||
|
<el-option v-if="isDpoOutput" label="DPO JSONL" value="dpo" />
|
||||||
<el-option label="Alpaca JSONL" value="alpaca_jsonl" />
|
<el-option label="Alpaca JSONL" value="alpaca_jsonl" />
|
||||||
<el-option label="JSONL" value="jsonl" />
|
<el-option label="JSONL" value="jsonl" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ function updateQualityRules(value: Array<string | number>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateOutputType(value: string | number | boolean | undefined) {
|
function updateOutputType(value: string | number | boolean | undefined) {
|
||||||
const outputType = value === 'reasoning' ? 'reasoning' : 'standard'
|
const outputType = value === 'reasoning' || value === 'dpo' ? value : 'standard'
|
||||||
emit('update:options', {
|
emit('update:options', {
|
||||||
...props.options,
|
...props.options,
|
||||||
outputType,
|
outputType,
|
||||||
@@ -153,6 +153,9 @@ function modelMeta(model: ModelItem) {
|
|||||||
<small v-if="options.outputType === 'reasoning'">
|
<small v-if="options.outputType === 'reasoning'">
|
||||||
当前使用思维链专用提示语;系统还会按所选详细程度约束推理结构
|
当前使用思维链专用提示语;系统还会按所选详细程度约束推理结构
|
||||||
</small>
|
</small>
|
||||||
|
<small v-else-if="options.outputType === 'dpo'">
|
||||||
|
当前使用 DPO 专用提示语,模型会同时生成 Chosen 与 Rejected 偏好回答
|
||||||
|
</small>
|
||||||
<small v-else>当前使用标准回答提示语,只要求问题和最终答案</small>
|
<small v-else>当前使用标准回答提示语,只要求问题和最终答案</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="prompt-input-wrapper">
|
<div class="prompt-input-wrapper">
|
||||||
@@ -237,7 +240,7 @@ function modelMeta(model: ModelItem) {
|
|||||||
<div class="output-type-row">
|
<div class="output-type-row">
|
||||||
<div class="field-copy">
|
<div class="field-copy">
|
||||||
<strong>输出类型</strong>
|
<strong>输出类型</strong>
|
||||||
<small>控制答案是否包含可用于推理模型训练的思维链内容</small>
|
<small>选择标准监督、思维链或 DPO 成对偏好数据</small>
|
||||||
</div>
|
</div>
|
||||||
<el-select
|
<el-select
|
||||||
class="output-type-select"
|
class="output-type-select"
|
||||||
@@ -247,6 +250,7 @@ function modelMeta(model: ModelItem) {
|
|||||||
>
|
>
|
||||||
<el-option label="标准回答" value="standard" />
|
<el-option label="标准回答" value="standard" />
|
||||||
<el-option label="思维链回答" value="reasoning" />
|
<el-option label="思维链回答" value="reasoning" />
|
||||||
|
<el-option label="DPO 偏好对" value="dpo" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="options.outputType === 'reasoning'" class="output-type-row">
|
<div v-if="options.outputType === 'reasoning'" class="output-type-row">
|
||||||
@@ -272,6 +276,9 @@ function modelMeta(model: ModelItem) {
|
|||||||
<template v-else>保留关键依据与必要步骤,</template>
|
<template v-else>保留关键依据与必要步骤,</template>
|
||||||
最终按 <code><think>推理过程</think></code> 加最终答案保存。
|
最终按 <code><think>推理过程</think></code> 加最终答案保存。
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="options.outputType === 'dpo'">
|
||||||
|
保存同一问题的 <code>chosen</code> 优选回答和 <code>rejected</code> 拒选回答,用于直接偏好优化训练。
|
||||||
|
</template>
|
||||||
<template v-else>仅保存最终答案,不包含推理过程。</template>
|
<template v-else>仅保存最终答案,不包含推理过程。</template>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import type { BulkResultRegenerationState, PreviewItem, ResultItem } from './types'
|
import type { BulkResultRegenerationState, PreviewItem, ResultItem } from './types'
|
||||||
|
import type { DataProcessOutputType } from '@/types/dataProcess'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: ResultItem[]
|
items: ResultItem[]
|
||||||
@@ -8,11 +9,12 @@ const props = defineProps<{
|
|||||||
selectedId: string | null
|
selectedId: string | null
|
||||||
regeneratingResultId: string | null
|
regeneratingResultId: string | null
|
||||||
bulkRegeneration: BulkResultRegenerationState
|
bulkRegeneration: BulkResultRegenerationState
|
||||||
|
outputType: DataProcessOutputType
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:selectedId': [value: string]
|
'update:selectedId': [value: string]
|
||||||
'update:field': [id: string, field: 'instruction' | 'input' | 'output', value: string]
|
'update:field': [id: string, field: 'instruction' | 'input' | 'output' | 'chosen' | 'rejected', value: string]
|
||||||
'regenerate:item': [id: string]
|
'regenerate:item': [id: string]
|
||||||
'regenerate:all': []
|
'regenerate:all': []
|
||||||
}>()
|
}>()
|
||||||
@@ -59,6 +61,11 @@ const selectedSourceMeta = computed(() => {
|
|||||||
const source = selectedSource.value
|
const source = selectedSource.value
|
||||||
if (!source) return ''
|
if (!source) return ''
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
|
if (source.sourcePages?.length) {
|
||||||
|
const first = source.sourcePages[0]
|
||||||
|
const last = source.sourcePages[source.sourcePages.length - 1]
|
||||||
|
parts.push(first === last ? `第 ${first} 页` : `第 ${first}–${last} 页`)
|
||||||
|
}
|
||||||
if (source.sourceStartLine != null) {
|
if (source.sourceStartLine != null) {
|
||||||
parts.push(
|
parts.push(
|
||||||
source.sourceEndLine != null && source.sourceEndLine !== source.sourceStartLine
|
source.sourceEndLine != null && source.sourceEndLine !== source.sourceStartLine
|
||||||
@@ -75,6 +82,8 @@ const filteredItems = computed(() => props.items.filter((item, index) => {
|
|||||||
const matchesSearch = !keyword
|
const matchesSearch = !keyword
|
||||||
|| item.instruction.toLowerCase().includes(keyword)
|
|| item.instruction.toLowerCase().includes(keyword)
|
||||||
|| item.output.toLowerCase().includes(keyword)
|
|| item.output.toLowerCase().includes(keyword)
|
||||||
|
|| item.chosen.toLowerCase().includes(keyword)
|
||||||
|
|| item.rejected.toLowerCase().includes(keyword)
|
||||||
|| String(index + 1).includes(keyword)
|
|| String(index + 1).includes(keyword)
|
||||||
return matchesSearch && (!invalidOnly.value || item.status === 'invalid')
|
return matchesSearch && (!invalidOnly.value || item.status === 'invalid')
|
||||||
}))
|
}))
|
||||||
@@ -135,7 +144,7 @@ function selectRelative(offset: number) {
|
|||||||
<span class="result-index">#{{ String(items.findIndex((entry) => entry.id === item.id) + 1).padStart(3, '0') }}</span>
|
<span class="result-index">#{{ String(items.findIndex((entry) => entry.id === item.id) + 1).padStart(3, '0') }}</span>
|
||||||
<span class="result-copy">
|
<span class="result-copy">
|
||||||
<strong>{{ item.instruction || '未填写指令' }}</strong>
|
<strong>{{ item.instruction || '未填写指令' }}</strong>
|
||||||
<small>{{ item.output || '未填写输出' }}</small>
|
<small>{{ outputType === 'dpo' ? (item.chosen || '未填写 Chosen') : (item.output || '未填写输出') }}</small>
|
||||||
</span>
|
</span>
|
||||||
<i v-if="itemRegenerating(item.id)" class="css-spinner" />
|
<i v-if="itemRegenerating(item.id)" class="css-spinner" />
|
||||||
<i
|
<i
|
||||||
@@ -207,7 +216,7 @@ function selectRelative(offset: number) {
|
|||||||
@update:model-value="emit('update:field', selectedItem.id, 'input', $event)"
|
@update:model-value="emit('update:field', selectedItem.id, 'input', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-editor">
|
<div v-if="outputType !== 'dpo'" class="field-editor">
|
||||||
<label>Output <em>必填</em></label>
|
<label>Output <em>必填</em></label>
|
||||||
<el-input
|
<el-input
|
||||||
:model-value="selectedItem.output"
|
:model-value="selectedItem.output"
|
||||||
@@ -217,6 +226,28 @@ function selectRelative(offset: number) {
|
|||||||
@update:model-value="emit('update:field', selectedItem.id, 'output', $event)"
|
@update:model-value="emit('update:field', selectedItem.id, 'output', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="field-editor dpo-field is-chosen">
|
||||||
|
<label>Chosen <em>优选回答,必填</em></label>
|
||||||
|
<el-input
|
||||||
|
:model-value="selectedItem.chosen"
|
||||||
|
:disabled="selectedItemRegenerating"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
@update:model-value="emit('update:field', selectedItem.id, 'chosen', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field-editor dpo-field is-rejected">
|
||||||
|
<label>Rejected <em>拒选回答,必填</em></label>
|
||||||
|
<el-input
|
||||||
|
:model-value="selectedItem.rejected"
|
||||||
|
:disabled="selectedItemRegenerating"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
@update:model-value="emit('update:field', selectedItem.id, 'rejected', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div v-if="selectedItem.error" class="validation-error">
|
<div v-if="selectedItem.error" class="validation-error">
|
||||||
<i class="fa fa-exclamation-circle" /> {{ selectedItem.error }}
|
<i class="fa fa-exclamation-circle" /> {{ selectedItem.error }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import type { UploadFile } from 'element-plus'
|
import type { UploadFile } from 'element-plus'
|
||||||
import type { ExternalDataSource, ProcessType, UploadedDataFile } from './types'
|
import type { ExternalDataSource, ProcessType, SourceMode, UploadedDataFile } from './types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
processType: ProcessType
|
processType: ProcessType
|
||||||
|
sourceMode: SourceMode
|
||||||
uploadedFiles: UploadedDataFile[]
|
uploadedFiles: UploadedDataFile[]
|
||||||
externalSource: ExternalDataSource
|
externalSource: ExternalDataSource
|
||||||
externalPulling: boolean
|
externalPulling: boolean
|
||||||
@@ -14,6 +15,7 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
'update:sourceMode': [value: SourceMode]
|
||||||
'update:externalSource': [value: ExternalDataSource]
|
'update:externalSource': [value: ExternalDataSource]
|
||||||
'file-change': [file: UploadFile]
|
'file-change': [file: UploadFile]
|
||||||
'remove-file': [uid: string | number]
|
'remove-file': [uid: string | number]
|
||||||
@@ -31,6 +33,14 @@ const AUTH_MODES = [
|
|||||||
{ value: 'basic', label: '账号密码' },
|
{ value: 'basic', label: '账号密码' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const SSL_MODES = [
|
||||||
|
{ value: 'disable', label: '关闭' },
|
||||||
|
{ value: 'prefer', label: '优先使用(默认)' },
|
||||||
|
{ value: 'require', label: '必须加密' },
|
||||||
|
{ value: 'verify-ca', label: '验证 CA' },
|
||||||
|
{ value: 'verify-full', label: '完整验证' },
|
||||||
|
]
|
||||||
|
|
||||||
const FILE_PAGE_SIZE = 10
|
const FILE_PAGE_SIZE = 10
|
||||||
const currentFilePage = ref(1)
|
const currentFilePage = ref(1)
|
||||||
type FileStage = 'queued' | 'uploading' | 'waiting' | 'processing' | 'success' | 'upload-failed' | 'preview-failed'
|
type FileStage = 'queued' | 'uploading' | 'waiting' | 'processing' | 'success' | 'upload-failed' | 'preview-failed'
|
||||||
@@ -45,7 +55,10 @@ const FILE_STAGE_META: Record<FileStage, { label: string; icon: string }> = {
|
|||||||
'preview-failed': { label: '切分失败', icon: 'fa-exclamation-circle' },
|
'preview-failed': { label: '切分失败', icon: 'fa-exclamation-circle' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const isExternal = computed(() => props.processType === 'external')
|
const isExternal = computed(() => props.sourceMode === 'external')
|
||||||
|
const sourceModeLocked = computed(() => (
|
||||||
|
props.uploadedFiles.length > 0 || props.sourceUploading || props.previewBuilding || props.externalPulling
|
||||||
|
))
|
||||||
|
|
||||||
const uploadAccept = computed(() => props.processType === 'unstructured'
|
const uploadAccept = computed(() => props.processType === 'unstructured'
|
||||||
? '.txt,.md,.markdown,.pdf,.docx,.pptx,.json,.jsonl,.ndjson'
|
? '.txt,.md,.markdown,.pdf,.docx,.pptx,.json,.jsonl,.ndjson'
|
||||||
@@ -124,6 +137,44 @@ function getFileError(file: UploadedDataFile) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="source-upload-step" aria-labelledby="source-upload-title">
|
<section class="source-upload-step" aria-labelledby="source-upload-title">
|
||||||
|
<div class="source-mode-section">
|
||||||
|
<div class="section-title-row">
|
||||||
|
<div>
|
||||||
|
<h3>数据来源</h3>
|
||||||
|
<p>在当前步骤选择本地文件上传或从外部系统拉取,处理类型保持不变</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="source-mode-options">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="source-mode-option"
|
||||||
|
:class="{ 'is-active': sourceMode === 'local' }"
|
||||||
|
:disabled="sourceModeLocked"
|
||||||
|
@click="emit('update:sourceMode', 'local')"
|
||||||
|
>
|
||||||
|
<i class="fa fa-upload" aria-hidden="true" />
|
||||||
|
<span><strong>本地上传</strong><small>从当前设备选择文件</small></span>
|
||||||
|
<i class="fa fa-check-circle selection-mark" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<el-tooltip
|
||||||
|
:content="processType === 'structured' ? '从标准化外部数据源配置拉取' : '当前仅支持把外部数据库拉取为结构化数据'"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="source-mode-option"
|
||||||
|
:class="{ 'is-active': sourceMode === 'external' }"
|
||||||
|
:disabled="sourceModeLocked || processType !== 'structured'"
|
||||||
|
@click="emit('update:sourceMode', 'external')"
|
||||||
|
>
|
||||||
|
<i class="fa fa-cloud-download" aria-hidden="true" />
|
||||||
|
<span><strong>外部数据源</strong><small>连接数据库并执行只读拉取</small></span>
|
||||||
|
<i class="fa fa-check-circle selection-mark" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<small v-if="sourceModeLocked" class="source-mode-lock-hint">已有来源文件时不可切换;删除现有文件后可重新选择。</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="isExternal" class="form-section external-section">
|
<div v-if="isExternal" class="form-section external-section">
|
||||||
<div class="section-title-row">
|
<div class="section-title-row">
|
||||||
<div>
|
<div>
|
||||||
@@ -202,6 +253,45 @@ function getFileError(file: UploadedDataFile) {
|
|||||||
@update:model-value="updateExternalField('limit', Number($event) || 0)"
|
@update:model-value="updateExternalField('limit', Number($event) || 0)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="SSL 模式">
|
||||||
|
<el-select
|
||||||
|
:model-value="externalSource.sslMode"
|
||||||
|
aria-label="SSL 模式"
|
||||||
|
@update:model-value="updateExternalField('sslMode', $event)"
|
||||||
|
>
|
||||||
|
<el-option v-for="item in SSL_MODES" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="连接超时">
|
||||||
|
<el-input-number
|
||||||
|
:model-value="externalSource.connectTimeoutSeconds"
|
||||||
|
:min="1"
|
||||||
|
:max="30"
|
||||||
|
controls-position="right"
|
||||||
|
aria-label="连接超时秒数"
|
||||||
|
@update:model-value="updateExternalField('connectTimeoutSeconds', Number($event) || 5)"
|
||||||
|
/>
|
||||||
|
<small>单位:秒</small>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="查询超时">
|
||||||
|
<el-input-number
|
||||||
|
:model-value="externalSource.statementTimeoutSeconds"
|
||||||
|
:min="1"
|
||||||
|
:max="300"
|
||||||
|
controls-position="right"
|
||||||
|
aria-label="查询超时秒数"
|
||||||
|
@update:model-value="updateExternalField('statementTimeoutSeconds', Number($event) || 30)"
|
||||||
|
/>
|
||||||
|
<small>单位:秒</small>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="落地文件名">
|
||||||
|
<el-input
|
||||||
|
:model-value="externalSource.fileName"
|
||||||
|
placeholder="external-data.jsonl"
|
||||||
|
aria-label="外部数据落地文件名"
|
||||||
|
@update:model-value="updateExternalField('fileName', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="只读查询语句" class="external-query-field">
|
<el-form-item label="只读查询语句" class="external-query-field">
|
||||||
<el-input
|
<el-input
|
||||||
:model-value="externalSource.query"
|
:model-value="externalSource.query"
|
||||||
@@ -314,7 +404,7 @@ function getFileError(file: UploadedDataFile) {
|
|||||||
v-if="uploadedFiles.length === 0"
|
v-if="uploadedFiles.length === 0"
|
||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="previewBuilding"
|
:disabled="previewBuilding || externalPulling"
|
||||||
@click="emit('use-sample')"
|
@click="emit('use-sample')"
|
||||||
>
|
>
|
||||||
使用示例数据
|
使用示例数据
|
||||||
@@ -326,7 +416,7 @@ function getFileError(file: UploadedDataFile) {
|
|||||||
drag
|
drag
|
||||||
multiple
|
multiple
|
||||||
:accept="uploadAccept"
|
:accept="uploadAccept"
|
||||||
:disabled="previewBuilding"
|
:disabled="previewBuilding || externalPulling"
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:on-change="(file: UploadFile) => emit('file-change', file)"
|
:on-change="(file: UploadFile) => emit('file-change', file)"
|
||||||
@@ -353,13 +443,13 @@ function getFileError(file: UploadedDataFile) {
|
|||||||
<el-upload
|
<el-upload
|
||||||
multiple
|
multiple
|
||||||
:accept="uploadAccept"
|
:accept="uploadAccept"
|
||||||
:disabled="previewBuilding"
|
:disabled="previewBuilding || externalPulling"
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:on-change="(file: UploadFile) => emit('file-change', file)"
|
:on-change="(file: UploadFile) => emit('file-change', file)"
|
||||||
aria-label="继续添加源数据文件"
|
aria-label="继续添加源数据文件"
|
||||||
>
|
>
|
||||||
<el-button size="small" type="primary" :disabled="previewBuilding">继续上传</el-button>
|
<el-button size="small" type="primary" :disabled="previewBuilding || externalPulling">继续上传</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -430,6 +520,68 @@ function getFileError(file: UploadedDataFile) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.source-mode-section {
|
||||||
|
padding-bottom: 22px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
border-bottom: 1px solid #edf0f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-mode-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-mode-option {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 76px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
color: #667085;
|
||||||
|
text-align: left;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dfe3ea;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
> .fa:first-child {
|
||||||
|
width: 28px;
|
||||||
|
color: #5b50f2;
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong { color: #344054; font-size: 14px; }
|
||||||
|
small { color: #8a93a3; font-size: 12px; }
|
||||||
|
.selection-mark { color: transparent; }
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: #fafaff;
|
||||||
|
border-color: #5b50f2;
|
||||||
|
box-shadow: 0 0 0 1px rgba(91, 80, 242, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active .selection-mark { color: #5b50f2; }
|
||||||
|
&:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-mode-lock-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 9px;
|
||||||
|
color: #8a93a3;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-section {
|
.form-section {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
@@ -672,6 +824,7 @@ function getFileError(file: UploadedDataFile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
.source-mode-options,
|
||||||
.external-section .external-grid {
|
.external-section .external-grid {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,20 +188,6 @@ defineExpose({ validate })
|
|||||||
</span>
|
</span>
|
||||||
<i class="fa fa-check-circle selection-mark" />
|
<i class="fa fa-check-circle selection-mark" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="type-option"
|
|
||||||
:class="{ 'is-active': processType === 'external' }"
|
|
||||||
:disabled="processTypeLocked"
|
|
||||||
@click="emit('update:processType', 'external')"
|
|
||||||
>
|
|
||||||
<span class="type-icon"><i class="fa fa-cloud-download" /></span>
|
|
||||||
<span>
|
|
||||||
<strong>外来数据源拉取</strong>
|
|
||||||
<small>适用于数据库、API 接口等需远程拉取的外部数据</small>
|
|
||||||
</span>
|
|
||||||
<i class="fa fa-check-circle selection-mark" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,7 +260,7 @@ defineExpose({ validate })
|
|||||||
|
|
||||||
.type-options {
|
.type-options {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,10 +73,21 @@ export const DEFAULT_REASONING_GENERATION_PROMPT = `你是一名具有深厚专
|
|||||||
3. 信息不足时如实处理:遇到来源信息缺失或相互矛盾时,需在推理中明确指出,并基于来源做出合理假设,但不得编造与来源无关的内容。
|
3. 信息不足时如实处理:遇到来源信息缺失或相互矛盾时,需在推理中明确指出,并基于来源做出合理假设,但不得编造与来源无关的内容。
|
||||||
4. 最终答案须能从前面的推理过程中自然得出,语言简洁、结构清晰。`
|
4. 最终答案须能从前面的推理过程中自然得出,语言简洁、结构清晰。`
|
||||||
|
|
||||||
export function defaultGenerationPrompt(outputType: 'standard' | 'reasoning') {
|
export const DEFAULT_DPO_GENERATION_PROMPT = `你是一名偏好数据构造专家。请基于下方来源内容,生成可用于直接偏好优化(DPO)的成对问答数据。
|
||||||
return outputType === 'reasoning'
|
|
||||||
? DEFAULT_REASONING_GENERATION_PROMPT
|
来源内容:
|
||||||
: DEFAULT_STANDARD_GENERATION_PROMPT
|
{{ content }}
|
||||||
|
|
||||||
|
任务要求:
|
||||||
|
1. 问题应聚焦来源内容的核心信息,并具有实际训练价值。
|
||||||
|
2. Chosen 必须准确、完整、清晰且严格忠于来源内容。
|
||||||
|
3. Rejected 应当表面合理但包含可辨认的缺陷,例如遗漏关键条件、事实偏差、逻辑不完整或表达含混;不得用乱码、空话或危险内容凑数。
|
||||||
|
4. Chosen 与 Rejected 必须明显不同,且都直接回答同一个问题。`
|
||||||
|
|
||||||
|
export function defaultGenerationPrompt(outputType: 'standard' | 'reasoning' | 'dpo') {
|
||||||
|
if (outputType === 'reasoning') return DEFAULT_REASONING_GENERATION_PROMPT
|
||||||
|
if (outputType === 'dpo') return DEFAULT_DPO_GENERATION_PROMPT
|
||||||
|
return DEFAULT_STANDARD_GENERATION_PROMPT
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBuiltInGenerationPrompt(value: string) {
|
export function isBuiltInGenerationPrompt(value: string) {
|
||||||
@@ -92,6 +103,7 @@ export function isBuiltInGenerationPrompt(value: string) {
|
|||||||
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_3,
|
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_3,
|
||||||
DEFAULT_STANDARD_GENERATION_PROMPT,
|
DEFAULT_STANDARD_GENERATION_PROMPT,
|
||||||
DEFAULT_REASONING_GENERATION_PROMPT,
|
DEFAULT_REASONING_GENERATION_PROMPT,
|
||||||
|
DEFAULT_DPO_GENERATION_PROMPT,
|
||||||
].map(normalize).includes(normalizedValue)
|
].map(normalize).includes(normalizedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +183,9 @@ function generationOptionsFromConfig(
|
|||||||
config: DataProcessConfig,
|
config: DataProcessConfig,
|
||||||
defaults: GenerationControlOptions,
|
defaults: GenerationControlOptions,
|
||||||
): GenerationControlOptions {
|
): GenerationControlOptions {
|
||||||
const outputType = configValue(config, 'output_type', defaults.outputType) === 'reasoning'
|
const configuredOutputType = configValue(config, 'output_type', defaults.outputType)
|
||||||
? 'reasoning'
|
const outputType = configuredOutputType === 'reasoning' || configuredOutputType === 'dpo'
|
||||||
|
? configuredOutputType
|
||||||
: 'standard'
|
: 'standard'
|
||||||
const configuredPrompt = String(
|
const configuredPrompt = String(
|
||||||
configValue(config, 'generation_prompt', defaults.generationPrompt),
|
configValue(config, 'generation_prompt', defaults.generationPrompt),
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import type { DataProcessExternalSourcePayload } from '@/api/modules/dataProcess'
|
||||||
|
import type { DataProcessConfig } from '@/types/dataProcess'
|
||||||
|
import type { ExternalDataSource, SourceMode } from './types'
|
||||||
|
|
||||||
|
export function createDefaultExternalSource(): ExternalDataSource {
|
||||||
|
return {
|
||||||
|
type: 'postgresql',
|
||||||
|
url: '',
|
||||||
|
authMode: 'none',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
limit: 1000,
|
||||||
|
connectTimeoutSeconds: 5,
|
||||||
|
statementTimeoutSeconds: 30,
|
||||||
|
sslMode: 'prefer',
|
||||||
|
query: '',
|
||||||
|
fileName: 'external-data.jsonl',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function externalSourcePayload(source: ExternalDataSource): DataProcessExternalSourcePayload {
|
||||||
|
return {
|
||||||
|
type: source.type,
|
||||||
|
url: source.url.trim(),
|
||||||
|
auth_mode: source.authMode,
|
||||||
|
username: source.username?.trim() || undefined,
|
||||||
|
password: source.password || undefined,
|
||||||
|
limit: source.limit,
|
||||||
|
connect_timeout_seconds: source.connectTimeoutSeconds,
|
||||||
|
statement_timeout_seconds: source.statementTimeoutSeconds,
|
||||||
|
ssl_mode: source.sslMode,
|
||||||
|
query: source.query?.trim() || undefined,
|
||||||
|
file_name: source.fileName || 'external-data.jsonl',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sourceConfigForBackend(mode: SourceMode, source: ExternalDataSource) {
|
||||||
|
const payload = externalSourcePayload(source)
|
||||||
|
const { password: _password, ...safeSource } = payload
|
||||||
|
return {
|
||||||
|
source_mode: mode,
|
||||||
|
external_source: mode === 'external' ? safeSource : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreExternalSourceConfig(config: DataProcessConfig): {
|
||||||
|
mode: SourceMode
|
||||||
|
source: ExternalDataSource
|
||||||
|
} {
|
||||||
|
const defaults = createDefaultExternalSource()
|
||||||
|
const value = config.external_source
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
return {
|
||||||
|
mode: config.source_mode === 'external' ? 'external' : 'local',
|
||||||
|
source: defaults,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const source = value as Record<string, unknown>
|
||||||
|
const sslMode = String(source.ssl_mode || '')
|
||||||
|
return {
|
||||||
|
mode: config.source_mode === 'external' ? 'external' : 'local',
|
||||||
|
source: {
|
||||||
|
...defaults,
|
||||||
|
url: String(source.url || ''),
|
||||||
|
authMode: source.auth_mode === 'basic' ? 'basic' : 'none',
|
||||||
|
username: String(source.username || ''),
|
||||||
|
limit: Number(source.limit) || defaults.limit,
|
||||||
|
connectTimeoutSeconds: Number(source.connect_timeout_seconds) || defaults.connectTimeoutSeconds,
|
||||||
|
statementTimeoutSeconds: Number(source.statement_timeout_seconds) || defaults.statementTimeoutSeconds,
|
||||||
|
sslMode: ['disable', 'prefer', 'require', 'verify-ca', 'verify-full'].includes(sslMode)
|
||||||
|
? sslMode as ExternalDataSource['sslMode']
|
||||||
|
: defaults.sslMode,
|
||||||
|
query: String(source.query || ''),
|
||||||
|
fileName: String(source.file_name || defaults.fileName),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
} from '@/types/dataProcess'
|
} from '@/types/dataProcess'
|
||||||
|
|
||||||
export type ProcessType = 'structured' | 'unstructured' | 'external'
|
export type ProcessType = 'structured' | 'unstructured' | 'external'
|
||||||
|
export type SourceMode = 'local' | 'external'
|
||||||
|
|
||||||
export type StepId = 'create' | 'model' | 'upload' | 'preview' | 'generate' | 'results'
|
export type StepId = 'create' | 'model' | 'upload' | 'preview' | 'generate' | 'results'
|
||||||
|
|
||||||
@@ -88,6 +89,9 @@ export interface ExternalDataSource {
|
|||||||
username?: string
|
username?: string
|
||||||
password?: string
|
password?: string
|
||||||
limit: number
|
limit: number
|
||||||
|
connectTimeoutSeconds: number
|
||||||
|
statementTimeoutSeconds: number
|
||||||
|
sslMode: 'disable' | 'prefer' | 'require' | 'verify-ca' | 'verify-full'
|
||||||
query?: string
|
query?: string
|
||||||
fileName?: string
|
fileName?: string
|
||||||
}
|
}
|
||||||
@@ -167,12 +171,18 @@ export interface ResultItem {
|
|||||||
instruction: string
|
instruction: string
|
||||||
input: string
|
input: string
|
||||||
output: string
|
output: string
|
||||||
|
chosen: string
|
||||||
|
rejected: string
|
||||||
originalInstruction: string
|
originalInstruction: string
|
||||||
originalInput: string
|
originalInput: string
|
||||||
originalOutput: string
|
originalOutput: string
|
||||||
|
originalChosen: string
|
||||||
|
originalRejected: string
|
||||||
savedInstruction: string
|
savedInstruction: string
|
||||||
savedInput: string
|
savedInput: string
|
||||||
savedOutput: string
|
savedOutput: string
|
||||||
|
savedChosen: string
|
||||||
|
savedRejected: string
|
||||||
savedStatus: 'valid' | 'modified' | 'invalid'
|
savedStatus: 'valid' | 'modified' | 'invalid'
|
||||||
status: 'valid' | 'modified' | 'invalid'
|
status: 'valid' | 'modified' | 'invalid'
|
||||||
error?: string
|
error?: string
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import {
|
|||||||
type DataProcessResult,
|
type DataProcessResult,
|
||||||
} from '@/api/modules/dataProcess'
|
} from '@/api/modules/dataProcess'
|
||||||
import type { BulkResultRegenerationState, GenerationState, ResultItem } from './types'
|
import type { BulkResultRegenerationState, GenerationState, ResultItem } from './types'
|
||||||
|
import type { DataProcessOutputType } from '@/types/dataProcess'
|
||||||
|
|
||||||
interface GenerationBindings {
|
interface GenerationBindings {
|
||||||
taskId: Ref<string | null>
|
taskId: Ref<string | null>
|
||||||
dirty: Ref<boolean>
|
dirty: Ref<boolean>
|
||||||
|
outputType: Ref<DataProcessOutputType>
|
||||||
beforeGenerate?: () => Promise<boolean | void>
|
beforeGenerate?: () => Promise<boolean | void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,12 +34,18 @@ function mapResult(item: DataProcessResult): ResultItem {
|
|||||||
instruction: item.instruction,
|
instruction: item.instruction,
|
||||||
input: item.input || '',
|
input: item.input || '',
|
||||||
output: item.output,
|
output: item.output,
|
||||||
|
chosen: item.chosen || item.output || '',
|
||||||
|
rejected: item.rejected || '',
|
||||||
originalInstruction: item.original_instruction ?? item.instruction,
|
originalInstruction: item.original_instruction ?? item.instruction,
|
||||||
originalInput: item.original_input ?? item.input ?? '',
|
originalInput: item.original_input ?? item.input ?? '',
|
||||||
originalOutput: item.original_output ?? item.output,
|
originalOutput: item.original_output ?? item.output,
|
||||||
|
originalChosen: item.original_chosen ?? item.chosen ?? item.output ?? '',
|
||||||
|
originalRejected: item.original_rejected ?? item.rejected ?? '',
|
||||||
savedInstruction: item.instruction,
|
savedInstruction: item.instruction,
|
||||||
savedInput: item.input || '',
|
savedInput: item.input || '',
|
||||||
savedOutput: item.output,
|
savedOutput: item.output,
|
||||||
|
savedChosen: item.chosen || item.output || '',
|
||||||
|
savedRejected: item.rejected || '',
|
||||||
savedStatus: item.status,
|
savedStatus: item.status,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
error: item.error || undefined,
|
error: item.error || undefined,
|
||||||
@@ -246,15 +254,34 @@ export function useDataProcessGeneration(bindings: GenerationBindings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateResultField(id: string, field: 'instruction' | 'input' | 'output', value: string) {
|
function updateResultField(
|
||||||
|
id: string,
|
||||||
|
field: 'instruction' | 'input' | 'output' | 'chosen' | 'rejected',
|
||||||
|
value: string,
|
||||||
|
) {
|
||||||
const item = results.value.find((entry) => entry.id === id)
|
const item = results.value.find((entry) => entry.id === id)
|
||||||
if (!item) return
|
if (!item) return
|
||||||
item[field] = value
|
item[field] = value
|
||||||
const valid = item.instruction.trim() && item.output.trim()
|
if (field === 'chosen') item.output = value
|
||||||
item.error = valid ? undefined : 'Instruction 和 Output 不能为空'
|
const isDpo = bindings.outputType.value === 'dpo'
|
||||||
|
const valid = isDpo
|
||||||
|
? Boolean(
|
||||||
|
item.instruction.trim()
|
||||||
|
&& item.chosen.trim()
|
||||||
|
&& item.rejected.trim()
|
||||||
|
&& item.chosen.trim() !== item.rejected.trim()
|
||||||
|
)
|
||||||
|
: Boolean(item.instruction.trim() && item.output.trim())
|
||||||
|
item.error = valid
|
||||||
|
? undefined
|
||||||
|
: isDpo
|
||||||
|
? 'Instruction、Chosen、Rejected 均不能为空,且两个回答不能相同'
|
||||||
|
: 'Instruction 和 Output 不能为空'
|
||||||
const changed = item.instruction !== item.originalInstruction
|
const changed = item.instruction !== item.originalInstruction
|
||||||
|| item.input !== item.originalInput
|
|| item.input !== item.originalInput
|
||||||
|| item.output !== item.originalOutput
|
|| item.output !== item.originalOutput
|
||||||
|
|| item.chosen !== item.originalChosen
|
||||||
|
|| item.rejected !== item.originalRejected
|
||||||
item.status = item.error ? 'invalid' : changed ? 'modified' : 'valid'
|
item.status = item.error ? 'invalid' : changed ? 'modified' : 'valid'
|
||||||
bindings.dirty.value = true
|
bindings.dirty.value = true
|
||||||
}
|
}
|
||||||
@@ -311,6 +338,8 @@ export function useDataProcessGeneration(bindings: GenerationBindings) {
|
|||||||
item.instruction !== item.savedInstruction
|
item.instruction !== item.savedInstruction
|
||||||
|| item.input !== item.savedInput
|
|| item.input !== item.savedInput
|
||||||
|| item.output !== item.savedOutput
|
|| item.output !== item.savedOutput
|
||||||
|
|| item.chosen !== item.savedChosen
|
||||||
|
|| item.rejected !== item.savedRejected
|
||||||
))
|
))
|
||||||
if (unsaved) {
|
if (unsaved) {
|
||||||
selectedResultId.value = unsaved.id
|
selectedResultId.value = unsaved.id
|
||||||
@@ -409,12 +438,16 @@ export function useDataProcessGeneration(bindings: GenerationBindings) {
|
|||||||
item.instruction !== item.savedInstruction
|
item.instruction !== item.savedInstruction
|
||||||
|| item.input !== item.savedInput
|
|| item.input !== item.savedInput
|
||||||
|| item.output !== item.savedOutput
|
|| item.output !== item.savedOutput
|
||||||
|
|| item.chosen !== item.savedChosen
|
||||||
|
|| item.rejected !== item.savedRejected
|
||||||
))
|
))
|
||||||
for (const item of changed) {
|
for (const item of changed) {
|
||||||
const saved = await updateDataProcessResult(taskId, item.id, {
|
const saved = await updateDataProcessResult(taskId, item.id, {
|
||||||
instruction: item.instruction,
|
instruction: item.instruction,
|
||||||
input: item.input,
|
input: item.input,
|
||||||
output: item.output,
|
output: item.output,
|
||||||
|
chosen: item.chosen,
|
||||||
|
rejected: item.rejected,
|
||||||
expected_updated_at: item.updatedAt,
|
expected_updated_at: item.updatedAt,
|
||||||
})
|
})
|
||||||
const index = results.value.findIndex((entry) => entry.id === item.id)
|
const index = results.value.findIndex((entry) => entry.id === item.id)
|
||||||
@@ -425,7 +458,13 @@ export function useDataProcessGeneration(bindings: GenerationBindings) {
|
|||||||
function validateResults() {
|
function validateResults() {
|
||||||
let firstInvalidId: string | null = null
|
let firstInvalidId: string | null = null
|
||||||
for (const item of results.value) {
|
for (const item of results.value) {
|
||||||
if (!item.instruction.trim() || !item.output.trim() || item.status === 'invalid') {
|
const requiredFieldsInvalid = bindings.outputType.value === 'dpo'
|
||||||
|
? !item.instruction.trim()
|
||||||
|
|| !item.chosen.trim()
|
||||||
|
|| !item.rejected.trim()
|
||||||
|
|| item.chosen.trim() === item.rejected.trim()
|
||||||
|
: !item.instruction.trim() || !item.output.trim()
|
||||||
|
if (requiredFieldsInvalid || item.status === 'invalid') {
|
||||||
item.error ||= '结果未通过后端质量校验,请修改后重新保存'
|
item.error ||= '结果未通过后端质量校验,请修改后重新保存'
|
||||||
item.status = 'invalid'
|
item.status = 'invalid'
|
||||||
firstInvalidId ??= item.id
|
firstInvalidId ??= item.id
|
||||||
|
|||||||
Reference in New Issue
Block a user