Add model labeling skill
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
# model-labeling
|
||||||
|
|
||||||
|
对已有数据调用线上模型接口批量打标。
|
||||||
|
|
||||||
|
典型输入:
|
||||||
|
|
||||||
|
- `output/records.jsonl`
|
||||||
|
- `output/training.jsonl`
|
||||||
|
- `output/eval_planning.csv`
|
||||||
|
- `output/records.csv`
|
||||||
|
|
||||||
|
典型流程:
|
||||||
|
|
||||||
|
1. 确认输入文件路径。
|
||||||
|
2. 确认线上模型 `generate` URL。
|
||||||
|
3. 用 `batch_label_model.py` 的 `dry_run=true` 识别格式。
|
||||||
|
4. 批量请求模型,输出 `output/model_predictions.jsonl`。
|
||||||
|
|
||||||
|
执行示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"script_path": "skills/model-labeling/scripts/batch_label_model.py",
|
||||||
|
"stdin": {
|
||||||
|
"input_path": "output/records.jsonl",
|
||||||
|
"model_url": "http://example/generate",
|
||||||
|
"output_path": "output/model_predictions.jsonl",
|
||||||
|
"parameters": {
|
||||||
|
"max_new_tokens": 64
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timeout_seconds": 600,
|
||||||
|
"max_output_chars": 20000
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
---
|
||||||
|
name: model-labeling
|
||||||
|
description: 对已有数据集调用线上模型 generate 接口批量打标,支持 canonical records、训练 jsonl、评测 CSV 和同事流转表格的统一归一化。
|
||||||
|
when_to_use: 当用户已有一份数据,或刚通过 product-data / online-mining-v2 生成数据后,希望指定线上模型 URL 批量请求模型、得到预测标签、对比真实标签或产出标注结果时使用。
|
||||||
|
aliases: batch-labeling, model-annotation, online-model-labeling, 模型打标
|
||||||
|
allowed_tools: read_file, write_file, grep_search, glob_search, ask_user_question, python_exec
|
||||||
|
---
|
||||||
|
|
||||||
|
# Model Labeling
|
||||||
|
|
||||||
|
使用这个 skill 处理“已有数据 -> 统一样本格式 -> 调线上模型接口批量打标 -> 输出预测结果”的流程。
|
||||||
|
|
||||||
|
线上模型 URL 经常变化,**不要猜 URL**。如果用户没有明确提供 `http://.../generate` 或等价接口地址,必须先询问用户。
|
||||||
|
|
||||||
|
## 能力组织
|
||||||
|
|
||||||
|
```text
|
||||||
|
skills/model-labeling/
|
||||||
|
SKILL.md
|
||||||
|
knowledge/
|
||||||
|
input_formats.md
|
||||||
|
scripts/
|
||||||
|
batch_label_model.py
|
||||||
|
```
|
||||||
|
|
||||||
|
`batch_label_model.py` 是 portable script,只依赖 Python 标准库。它会:
|
||||||
|
|
||||||
|
- 识别 canonical records JSON/JSONL。
|
||||||
|
- 识别 product-data 导出的训练 JSONL。
|
||||||
|
- 识别 eval/planning CSV,优先使用 `newPrompt`。
|
||||||
|
- 识别同事流转 CSV,使用 `query`、`prev_session`、`context`、`function`。
|
||||||
|
- 对未知格式返回结构化错误,要求用户提供字段映射,不要擅自转换。
|
||||||
|
- 调用用户提供的模型接口,默认请求体为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"inputs": "<prompt>",
|
||||||
|
"parameters": {
|
||||||
|
"max_new_tokens": 64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 交互规则
|
||||||
|
|
||||||
|
开始执行前必须确认:
|
||||||
|
|
||||||
|
1. **输入数据路径**:用户给出的文件路径,或上一轮产物路径。
|
||||||
|
2. **模型接口 URL**:必须是用户明确提供的 URL;没有就问。
|
||||||
|
3. **输入格式是否可识别**:canonical records、训练 jsonl、eval CSV、同事流转表格可以直接处理。
|
||||||
|
4. **未知格式的字段映射**:如果脚本提示 unknown format,需要问用户:
|
||||||
|
- 哪列是 query?
|
||||||
|
- 哪列是 prompt?
|
||||||
|
- 哪列是真实标签?
|
||||||
|
- 是否有历史上下文、context?
|
||||||
|
5. **输出路径**:默认写到当前 session 的 `output/model_predictions.jsonl`。
|
||||||
|
|
||||||
|
不要在没有 URL 的情况下开始打标。不要把临时结果写到项目根目录。所有输出优先放当前 session 的 `output/`。
|
||||||
|
|
||||||
|
## 推荐流程
|
||||||
|
|
||||||
|
### 1. 找到输入文件
|
||||||
|
|
||||||
|
如果用户说“刚才生成的数据”“这份数据”,先用会话文件面板或 `glob_search` / `read_file` 找到实际路径。常见路径:
|
||||||
|
|
||||||
|
```text
|
||||||
|
output/records.jsonl
|
||||||
|
output/training.jsonl
|
||||||
|
output/eval_planning.csv
|
||||||
|
output/records.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 确认模型 URL
|
||||||
|
|
||||||
|
如果用户没有提供 URL,直接问:
|
||||||
|
|
||||||
|
```text
|
||||||
|
请提供这次要调用的线上模型 generate 接口 URL,例如 http://.../generate。
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 `ask_user_question` 可用,优先使用;不可用就普通回复提问并停止。
|
||||||
|
|
||||||
|
### 3. 先 dry run 识别格式
|
||||||
|
|
||||||
|
先执行一次 `dry_run=true`,只识别格式和样例,不请求模型:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"script_path": "skills/model-labeling/scripts/batch_label_model.py",
|
||||||
|
"stdin": {
|
||||||
|
"input_path": "output/records.jsonl",
|
||||||
|
"model_url": "http://example/generate",
|
||||||
|
"dry_run": true,
|
||||||
|
"max_records": 3
|
||||||
|
},
|
||||||
|
"timeout_seconds": 60,
|
||||||
|
"max_output_chars": 20000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果返回 `ok=false` 且 `needs_mapping=true`,必须把错误和已识别字段展示给用户,让用户说明映射。
|
||||||
|
|
||||||
|
### 4. 批量请求模型
|
||||||
|
|
||||||
|
确认格式后执行:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"script_path": "skills/model-labeling/scripts/batch_label_model.py",
|
||||||
|
"stdin": {
|
||||||
|
"input_path": "output/records.jsonl",
|
||||||
|
"model_url": "http://example/generate",
|
||||||
|
"output_path": "output/model_predictions.jsonl",
|
||||||
|
"parameters": {
|
||||||
|
"max_new_tokens": 64
|
||||||
|
},
|
||||||
|
"timeout_seconds": 60
|
||||||
|
},
|
||||||
|
"timeout_seconds": 600,
|
||||||
|
"max_output_chars": 20000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`output_path` 使用相对 `output/...`,平台会路由到当前 session output 目录。
|
||||||
|
|
||||||
|
### 5. 展示结果
|
||||||
|
|
||||||
|
执行完成后,简短展示:
|
||||||
|
|
||||||
|
- 输入格式。
|
||||||
|
- 处理条数、成功数、失败数。
|
||||||
|
- 输出路径。
|
||||||
|
- 抽 3 条预测样例。
|
||||||
|
|
||||||
|
如果存在真实标签,说明输出里包含 `gold_label`,后续可以继续做准确率或错误分析。
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
|
||||||
|
默认输出 JSONL,一行一条紧凑 JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"index":0,"request_id":"...","query":"...","gold_label":"complex=false\nAgent(tag=\"地图导航\")","prediction":"...","ok":true,"latency_ms":123}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果请求失败:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"index":0,"query":"...","gold_label":"...","prediction":"","ok":false,"error":"HTTP 500 ...","latency_ms":123}
|
||||||
|
```
|
||||||
|
|
||||||
|
默认不把完整 prompt 写入结果,避免文件过大。如确实需要排查,可传 `include_prompt=true`。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- URL、鉴权 header、特殊请求体字段都以用户提供为准。
|
||||||
|
- 默认接口字段是 `inputs` 和 `parameters`;如果用户说明接口不同,需要在脚本输入里传 `request_template`。
|
||||||
|
- 大批量请求前先小样本 dry run。
|
||||||
|
- 打标脚本只负责请求模型和记录预测结果,不负责修改原始数据。
|
||||||
|
- 后续准确率统计、错误聚类、补数计划可以再交给其他 skill。
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Model Labeling 输入格式
|
||||||
|
|
||||||
|
本 skill 的脚本会把不同来源的数据统一成 sample:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"request_id": "",
|
||||||
|
"query": "",
|
||||||
|
"prompt": "",
|
||||||
|
"gold_label": "",
|
||||||
|
"source_format": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## canonical records
|
||||||
|
|
||||||
|
识别条件:
|
||||||
|
|
||||||
|
- JSONL 每行是对象,或 JSON 数组 / `{ "records": [...] }`。
|
||||||
|
- 对象包含 `turn.query`。
|
||||||
|
|
||||||
|
字段来源:
|
||||||
|
|
||||||
|
- `query`:`turn.query`
|
||||||
|
- `request_id`:`source.request_id`
|
||||||
|
- `gold_label`:`complex=true/false` + `label.target`
|
||||||
|
- `prompt`:按 product-data 的 planning prompt 规则生成
|
||||||
|
|
||||||
|
## training jsonl
|
||||||
|
|
||||||
|
识别条件:
|
||||||
|
|
||||||
|
- 每行是对象。
|
||||||
|
- 包含 `instruction` 或 `system`。
|
||||||
|
|
||||||
|
字段来源:
|
||||||
|
|
||||||
|
- `prompt`:`system + instruction` 包成 chat template;如果已有 `prompt` 则直接使用。
|
||||||
|
- `gold_label`:`output`
|
||||||
|
- `query`:尽力从 `[当前query]` 后的 `用户:` 抽取。
|
||||||
|
|
||||||
|
## eval/planning CSV
|
||||||
|
|
||||||
|
识别条件:
|
||||||
|
|
||||||
|
- CSV 表头包含 `newPrompt` 或 `query`。
|
||||||
|
|
||||||
|
字段来源:
|
||||||
|
|
||||||
|
- `prompt`:优先 `newPrompt`
|
||||||
|
- `query`:`query`
|
||||||
|
- `gold_label`:`code标签`,如果有 `complex` 列则组合成两行
|
||||||
|
|
||||||
|
## 同事流转 CSV
|
||||||
|
|
||||||
|
识别条件:
|
||||||
|
|
||||||
|
- CSV 表头包含 `query` 和 `function`。
|
||||||
|
|
||||||
|
字段来源:
|
||||||
|
|
||||||
|
- `query`:`query`
|
||||||
|
- `request_id`:`request_id`
|
||||||
|
- `gold_label`:`function`
|
||||||
|
- `prompt`:根据 `query`、`prev_session`、`context` 生成 planning prompt
|
||||||
|
|
||||||
|
## 未知格式
|
||||||
|
|
||||||
|
如果不能识别,脚本返回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": false,
|
||||||
|
"needs_mapping": true,
|
||||||
|
"columns": ["..."],
|
||||||
|
"error": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
此时必须询问用户字段映射,不要猜。
|
||||||
+600
@@ -0,0 +1,600 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
"""批量调用线上模型 generate 接口给数据打标。
|
||||||
|
|
||||||
|
输入通过 stdin 或 --input 传 JSON 对象,输出稳定 JSON 对象到 stdout。
|
||||||
|
脚本只依赖 Python 标准库,便于在本地、Linux runtime 用户或远程工作区迁移执行。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from io import StringIO
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SYSTEM_PROMPT = "你是小爱同学,中文智能语音助手。"
|
||||||
|
DEFAULT_PARAMETERS = {"max_new_tokens": 64}
|
||||||
|
|
||||||
|
|
||||||
|
class LabelingError(ValueError):
|
||||||
|
"""输入参数或数据格式错误。"""
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
payload = load_payload()
|
||||||
|
result = run(payload)
|
||||||
|
emit({"ok": True, **result})
|
||||||
|
return 0
|
||||||
|
except LabelingError as exc:
|
||||||
|
emit({"ok": False, "error": str(exc), **getattr(exc, "extra", {})})
|
||||||
|
return 1
|
||||||
|
except Exception as exc: # noqa: BLE001 - CLI 需要稳定 JSON 错误
|
||||||
|
emit({"ok": False, "error": str(exc)})
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def load_payload() -> dict[str, Any]:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--input", "-i", help="JSON 参数文件;不传则从 stdin 读取")
|
||||||
|
args = parser.parse_args()
|
||||||
|
text = Path(args.input).read_text(encoding="utf-8") if args.input else sys.stdin.read()
|
||||||
|
if not text.strip():
|
||||||
|
raise LabelingError("input JSON is required")
|
||||||
|
payload = json.loads(text)
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise LabelingError("input JSON must be an object")
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def emit(payload: dict[str, Any]) -> None:
|
||||||
|
print(json.dumps(payload, ensure_ascii=False, separators=(",", ":")))
|
||||||
|
|
||||||
|
|
||||||
|
def run(payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
input_path = require_string(payload, "input_path")
|
||||||
|
model_url = require_string(payload, "model_url")
|
||||||
|
dry_run = bool(payload.get("dry_run", False))
|
||||||
|
include_prompt = bool(payload.get("include_prompt", False))
|
||||||
|
max_records = optional_int(payload.get("max_records"))
|
||||||
|
start_index = int(payload.get("start_index") or 0)
|
||||||
|
timeout_seconds = float(payload.get("timeout_seconds") or 60)
|
||||||
|
output_path = str(payload.get("output_path") or "output/model_predictions.jsonl")
|
||||||
|
parameters = payload.get("parameters")
|
||||||
|
if parameters is None:
|
||||||
|
parameters = dict(DEFAULT_PARAMETERS)
|
||||||
|
if not isinstance(parameters, dict):
|
||||||
|
raise LabelingError("parameters must be an object")
|
||||||
|
headers = payload.get("headers")
|
||||||
|
if headers is None:
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
if not isinstance(headers, dict):
|
||||||
|
raise LabelingError("headers must be an object")
|
||||||
|
request_template = payload.get("request_template")
|
||||||
|
if request_template is not None and not isinstance(request_template, dict):
|
||||||
|
raise LabelingError("request_template must be an object")
|
||||||
|
|
||||||
|
source = read_input_file(input_path)
|
||||||
|
samples = normalize_samples(
|
||||||
|
source,
|
||||||
|
field_mapping=payload.get("field_mapping"),
|
||||||
|
system_prompt=str(payload.get("system_prompt") or DEFAULT_SYSTEM_PROMPT),
|
||||||
|
session_num=int(payload.get("session_num") or 5),
|
||||||
|
session_time_minutes=int(payload.get("session_time_minutes") or 5),
|
||||||
|
)
|
||||||
|
if start_index:
|
||||||
|
samples = samples[start_index:]
|
||||||
|
if max_records is not None:
|
||||||
|
samples = samples[:max_records]
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"input_path": input_path,
|
||||||
|
"source_format": source["format"],
|
||||||
|
"total_samples": len(samples),
|
||||||
|
"sample_preview": preview_samples(samples),
|
||||||
|
}
|
||||||
|
if dry_run:
|
||||||
|
return {**summary, "dry_run": True}
|
||||||
|
|
||||||
|
if not model_url.startswith(("http://", "https://")):
|
||||||
|
raise LabelingError("model_url must start with http:// or https://")
|
||||||
|
results: list[dict[str, Any]] = []
|
||||||
|
ok_count = 0
|
||||||
|
output = resolve_runtime_path(output_path)
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with output.open("w", encoding="utf-8") as file:
|
||||||
|
for sample in samples:
|
||||||
|
item = request_one(
|
||||||
|
sample,
|
||||||
|
model_url=model_url,
|
||||||
|
parameters=parameters,
|
||||||
|
headers={str(k): str(v) for k, v in headers.items()},
|
||||||
|
timeout_seconds=timeout_seconds,
|
||||||
|
request_template=request_template,
|
||||||
|
include_prompt=include_prompt,
|
||||||
|
)
|
||||||
|
if item.get("ok"):
|
||||||
|
ok_count += 1
|
||||||
|
results.append(item)
|
||||||
|
file.write(json.dumps(item, ensure_ascii=False, separators=(",", ":")) + "\n")
|
||||||
|
|
||||||
|
return {
|
||||||
|
**summary,
|
||||||
|
"dry_run": False,
|
||||||
|
"output_path": str(output),
|
||||||
|
"success_count": ok_count,
|
||||||
|
"failure_count": len(results) - ok_count,
|
||||||
|
"result_preview": results[:3],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def request_one(
|
||||||
|
sample: dict[str, Any],
|
||||||
|
*,
|
||||||
|
model_url: str,
|
||||||
|
parameters: dict[str, Any],
|
||||||
|
headers: dict[str, str],
|
||||||
|
timeout_seconds: float,
|
||||||
|
request_template: dict[str, Any] | None,
|
||||||
|
include_prompt: bool,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
prompt = str(sample["prompt"])
|
||||||
|
body = build_request_body(prompt, parameters, request_template)
|
||||||
|
started = time.time()
|
||||||
|
base = {
|
||||||
|
"index": sample["index"],
|
||||||
|
"request_id": sample.get("request_id", ""),
|
||||||
|
"query": sample.get("query", ""),
|
||||||
|
"gold_label": sample.get("gold_label", ""),
|
||||||
|
"source_format": sample.get("source_format", ""),
|
||||||
|
}
|
||||||
|
if include_prompt:
|
||||||
|
base["prompt"] = prompt
|
||||||
|
try:
|
||||||
|
request = urllib.request.Request(
|
||||||
|
model_url,
|
||||||
|
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
|
||||||
|
headers=headers,
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
|
||||||
|
text = response.read().decode("utf-8", errors="replace")
|
||||||
|
status = getattr(response, "status", 200)
|
||||||
|
latency_ms = int((time.time() - started) * 1000)
|
||||||
|
decoded = try_json(text)
|
||||||
|
return {
|
||||||
|
**base,
|
||||||
|
"prediction": extract_prediction(decoded, text),
|
||||||
|
"ok": 200 <= int(status) < 300,
|
||||||
|
"status": int(status),
|
||||||
|
"latency_ms": latency_ms,
|
||||||
|
"response": decoded if decoded is not None else text,
|
||||||
|
}
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
text = exc.read().decode("utf-8", errors="replace")
|
||||||
|
return {
|
||||||
|
**base,
|
||||||
|
"prediction": extract_prediction(try_json(text), text),
|
||||||
|
"ok": False,
|
||||||
|
"status": exc.code,
|
||||||
|
"latency_ms": int((time.time() - started) * 1000),
|
||||||
|
"error": f"HTTP {exc.code}: {text[:500]}",
|
||||||
|
}
|
||||||
|
except Exception as exc: # noqa: BLE001 - 单条失败不中断整体批次
|
||||||
|
return {
|
||||||
|
**base,
|
||||||
|
"prediction": "",
|
||||||
|
"ok": False,
|
||||||
|
"latency_ms": int((time.time() - started) * 1000),
|
||||||
|
"error": str(exc),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_request_body(
|
||||||
|
prompt: str,
|
||||||
|
parameters: dict[str, Any],
|
||||||
|
request_template: dict[str, Any] | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if request_template is None:
|
||||||
|
return {"inputs": prompt, "parameters": parameters}
|
||||||
|
return replace_placeholders(request_template, {"prompt": prompt, "parameters": parameters})
|
||||||
|
|
||||||
|
|
||||||
|
def replace_placeholders(value: Any, variables: dict[str, Any]) -> Any:
|
||||||
|
if isinstance(value, str):
|
||||||
|
if value == "{{prompt}}":
|
||||||
|
return variables["prompt"]
|
||||||
|
if value == "{{parameters}}":
|
||||||
|
return variables["parameters"]
|
||||||
|
return value.replace("{{prompt}}", str(variables["prompt"]))
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [replace_placeholders(item, variables) for item in value]
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {key: replace_placeholders(item, variables) for key, item in value.items()}
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def read_input_file(path: str) -> dict[str, Any]:
|
||||||
|
file_path = resolve_runtime_path(path)
|
||||||
|
if not file_path.exists():
|
||||||
|
raise LabelingError(f"input_path not found: {path}")
|
||||||
|
suffix = file_path.suffix.lower()
|
||||||
|
text = file_path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
if suffix in {".csv", ".tsv"}:
|
||||||
|
delimiter = "\t" if suffix == ".tsv" else ","
|
||||||
|
rows = list(csv.DictReader(StringIO(text), delimiter=delimiter))
|
||||||
|
return {"format": "table", "path": str(file_path), "rows": rows, "columns": list(rows[0].keys()) if rows else []}
|
||||||
|
if suffix == ".jsonl":
|
||||||
|
rows = [json.loads(line) for line in text.splitlines() if line.strip()]
|
||||||
|
return {"format": "jsonl", "path": str(file_path), "rows": rows}
|
||||||
|
decoded = json.loads(text)
|
||||||
|
if isinstance(decoded, dict) and isinstance(decoded.get("records"), list):
|
||||||
|
decoded = decoded["records"]
|
||||||
|
if isinstance(decoded, list):
|
||||||
|
return {"format": "json", "path": str(file_path), "rows": decoded}
|
||||||
|
raise_with_mapping("JSON input must be an array or {records:[...]}", columns=list(decoded.keys()) if isinstance(decoded, dict) else [])
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_samples(
|
||||||
|
source: dict[str, Any],
|
||||||
|
*,
|
||||||
|
field_mapping: Any,
|
||||||
|
system_prompt: str,
|
||||||
|
session_num: int,
|
||||||
|
session_time_minutes: int,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
rows = source.get("rows")
|
||||||
|
if not isinstance(rows, list) or not rows:
|
||||||
|
raise LabelingError("input file contains no rows")
|
||||||
|
if isinstance(field_mapping, dict):
|
||||||
|
return samples_from_mapping(rows, field_mapping, source["format"], system_prompt)
|
||||||
|
first = rows[0]
|
||||||
|
if not isinstance(first, dict):
|
||||||
|
raise_with_mapping("rows must be objects")
|
||||||
|
if is_canonical_record(first):
|
||||||
|
return [
|
||||||
|
sample_from_record(index, row, system_prompt, session_num, session_time_minutes)
|
||||||
|
for index, row in enumerate(rows)
|
||||||
|
if isinstance(row, dict)
|
||||||
|
]
|
||||||
|
if is_training_row(first):
|
||||||
|
return [sample_from_training(index, row, system_prompt) for index, row in enumerate(rows) if isinstance(row, dict)]
|
||||||
|
if source["format"] == "table":
|
||||||
|
columns = list(first.keys())
|
||||||
|
if "newPrompt" in columns:
|
||||||
|
return [sample_from_eval_row(index, row) for index, row in enumerate(rows) if isinstance(row, dict)]
|
||||||
|
if "query" in columns and "function" in columns:
|
||||||
|
return [sample_from_flow_row(index, row, system_prompt) for index, row in enumerate(rows) if isinstance(row, dict)]
|
||||||
|
raise_with_mapping("unrecognized table format", columns=columns)
|
||||||
|
raise_with_mapping("unrecognized JSON/JSONL format", columns=list(first.keys()))
|
||||||
|
|
||||||
|
|
||||||
|
def sample_from_record(
|
||||||
|
index: int,
|
||||||
|
record: dict[str, Any],
|
||||||
|
system_prompt: str,
|
||||||
|
session_num: int,
|
||||||
|
session_time_minutes: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
source = record.get("source") if isinstance(record.get("source"), dict) else {}
|
||||||
|
turn = record.get("turn") if isinstance(record.get("turn"), dict) else {}
|
||||||
|
return {
|
||||||
|
"index": index,
|
||||||
|
"request_id": str(source.get("request_id") or ""),
|
||||||
|
"query": str(turn.get("query") or ""),
|
||||||
|
"prompt": build_planning_prompt(record, system_prompt, session_num, session_time_minutes),
|
||||||
|
"gold_label": combined_label(record),
|
||||||
|
"source_format": "canonical_record_v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sample_from_training(index: int, row: dict[str, Any], system_prompt: str) -> dict[str, Any]:
|
||||||
|
prompt = str(row.get("prompt") or "")
|
||||||
|
if not prompt:
|
||||||
|
system = str(row.get("system") or system_prompt)
|
||||||
|
instruction = str(row.get("instruction") or row.get("input") or "")
|
||||||
|
prompt = wrap_chat_prompt(system, instruction)
|
||||||
|
return {
|
||||||
|
"index": index,
|
||||||
|
"request_id": str(row.get("request_id") or ""),
|
||||||
|
"query": extract_query_from_prompt(prompt),
|
||||||
|
"prompt": prompt,
|
||||||
|
"gold_label": str(row.get("output") or row.get("target") or ""),
|
||||||
|
"source_format": "training_jsonl",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sample_from_eval_row(index: int, row: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
gold = str(row.get("code标签") or row.get("function") or row.get("target") or "")
|
||||||
|
complex_value = row.get("complex")
|
||||||
|
if complex_value not in (None, "") and not gold.startswith("complex="):
|
||||||
|
gold = f"complex={normalize_bool_literal(complex_value)}\n{gold}".rstrip()
|
||||||
|
prompt = str(row.get("newPrompt") or row.get("prompt") or "")
|
||||||
|
query = str(row.get("query") or "")
|
||||||
|
if not prompt:
|
||||||
|
prompt = build_planning_prompt(minimal_record(query, {}, [], gold), DEFAULT_SYSTEM_PROMPT, 5, 5)
|
||||||
|
return {
|
||||||
|
"index": index,
|
||||||
|
"request_id": str(row.get("request_id") or ""),
|
||||||
|
"query": query,
|
||||||
|
"prompt": prompt,
|
||||||
|
"gold_label": gold,
|
||||||
|
"source_format": "eval_csv",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sample_from_flow_row(index: int, row: dict[str, Any], system_prompt: str) -> dict[str, Any]:
|
||||||
|
record = record_from_flow_row(row)
|
||||||
|
return {
|
||||||
|
"index": index,
|
||||||
|
"request_id": str(row.get("request_id") or ""),
|
||||||
|
"query": str(row.get("query") or ""),
|
||||||
|
"prompt": build_planning_prompt(record, system_prompt, 5, 5),
|
||||||
|
"gold_label": str(row.get("function") or ""),
|
||||||
|
"source_format": "flow_csv",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def samples_from_mapping(
|
||||||
|
rows: list[Any],
|
||||||
|
mapping: dict[str, Any],
|
||||||
|
source_format: str,
|
||||||
|
system_prompt: str,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
query_field = str(mapping.get("query") or "")
|
||||||
|
prompt_field = str(mapping.get("prompt") or "")
|
||||||
|
label_field = str(mapping.get("label") or mapping.get("target") or "")
|
||||||
|
request_id_field = str(mapping.get("request_id") or "")
|
||||||
|
if not query_field and not prompt_field:
|
||||||
|
raise LabelingError("field_mapping must provide query or prompt")
|
||||||
|
samples: list[dict[str, Any]] = []
|
||||||
|
for index, row in enumerate(rows):
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
prompt = str(row.get(prompt_field) or "")
|
||||||
|
query = str(row.get(query_field) or "")
|
||||||
|
if not prompt:
|
||||||
|
record = minimal_record(query, {}, [], str(row.get(label_field) or ""))
|
||||||
|
prompt = build_planning_prompt(record, system_prompt, 5, 5)
|
||||||
|
samples.append(
|
||||||
|
{
|
||||||
|
"index": index,
|
||||||
|
"request_id": str(row.get(request_id_field) or ""),
|
||||||
|
"query": query or extract_query_from_prompt(prompt),
|
||||||
|
"prompt": prompt,
|
||||||
|
"gold_label": str(row.get(label_field) or ""),
|
||||||
|
"source_format": f"{source_format}_mapped",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return samples
|
||||||
|
|
||||||
|
|
||||||
|
def is_canonical_record(row: dict[str, Any]) -> bool:
|
||||||
|
return isinstance(row.get("turn"), dict) and bool(row["turn"].get("query"))
|
||||||
|
|
||||||
|
|
||||||
|
def is_training_row(row: dict[str, Any]) -> bool:
|
||||||
|
return any(key in row for key in ("instruction", "system", "output", "prompt"))
|
||||||
|
|
||||||
|
|
||||||
|
def build_planning_prompt(
|
||||||
|
record: dict[str, Any],
|
||||||
|
system_prompt: str,
|
||||||
|
session_num: int,
|
||||||
|
session_time_minutes: int,
|
||||||
|
) -> str:
|
||||||
|
instruction = build_training_instruction(record, session_num, session_time_minutes)
|
||||||
|
return wrap_chat_prompt(system_prompt, instruction)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_chat_prompt(system_prompt: str, instruction: str) -> str:
|
||||||
|
return (
|
||||||
|
f"<|im_start|>system\n{system_prompt}<|im_end|>\n"
|
||||||
|
f"<|im_start|>user\n{instruction}<|im_end|>\n"
|
||||||
|
"<|im_start|>assistant\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_training_instruction(record: dict[str, Any], session_num: int, session_time_minutes: int) -> str:
|
||||||
|
turn = record.get("turn") if isinstance(record.get("turn"), dict) else {}
|
||||||
|
query = str(turn.get("query") or "")
|
||||||
|
context = record.get("context") if isinstance(record.get("context"), dict) else {}
|
||||||
|
prev_session = record.get("prev_session") if isinstance(record.get("prev_session"), list) else []
|
||||||
|
current_ts = optional_int(turn.get("timestamp"))
|
||||||
|
history = render_history(prev_session, current_ts, session_num, session_time_minutes)
|
||||||
|
return (
|
||||||
|
"请参考用户的[当前query]、[对话历史]、[知识注入]、[系统状态]识别出[当前query]的[function]结果,[function]是python的code形式。\n"
|
||||||
|
"[知识注入]\n"
|
||||||
|
f"{json.dumps({'location': str(context.get('location') or ''), 'rag': str(context.get('rag') or '')}, ensure_ascii=False, indent=0)}\n"
|
||||||
|
"[系统状态]\n"
|
||||||
|
"{}\n"
|
||||||
|
"[对话历史]\n"
|
||||||
|
f"{history}"
|
||||||
|
"[当前query]\n"
|
||||||
|
f"用户: {query}\n"
|
||||||
|
"[function]\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render_history(prev_session: list[Any], current_ts: int | None, session_num: int, session_time_minutes: int) -> str:
|
||||||
|
usable: list[dict[str, Any]] = []
|
||||||
|
for item in prev_session[-session_num:]:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
ts = optional_int(item.get("timestamp"))
|
||||||
|
if current_ts is not None and ts is not None:
|
||||||
|
if abs(current_ts - ts) > session_time_minutes * 60_000:
|
||||||
|
continue
|
||||||
|
usable.append(item)
|
||||||
|
if not usable:
|
||||||
|
return ""
|
||||||
|
lines: list[str] = []
|
||||||
|
for item in usable:
|
||||||
|
query = str(item.get("query") or "").strip()
|
||||||
|
tts = str(item.get("tts") or "").strip()
|
||||||
|
if query:
|
||||||
|
lines.append(f"用户: {query}")
|
||||||
|
if tts:
|
||||||
|
lines.append(f"小爱: {tts}")
|
||||||
|
return "\n".join(lines) + ("\n" if lines else "")
|
||||||
|
|
||||||
|
|
||||||
|
def record_from_flow_row(row: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
prev_session = parse_json_cell(row.get("prev_session"), default=[])
|
||||||
|
context = parse_json_cell(row.get("context"), default={})
|
||||||
|
return minimal_record(
|
||||||
|
str(row.get("query") or ""),
|
||||||
|
context if isinstance(context, dict) else {},
|
||||||
|
prev_session if isinstance(prev_session, list) else [],
|
||||||
|
str(row.get("function") or ""),
|
||||||
|
request_id=str(row.get("request_id") or ""),
|
||||||
|
timestamp=optional_int(row.get("timestamp")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def minimal_record(
|
||||||
|
query: str,
|
||||||
|
context: dict[str, Any],
|
||||||
|
prev_session: list[Any],
|
||||||
|
target: str,
|
||||||
|
*,
|
||||||
|
request_id: str = "",
|
||||||
|
timestamp: int | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"source": {"request_id": request_id, "timestamp": timestamp},
|
||||||
|
"turn": {"query": query, "timestamp": timestamp},
|
||||||
|
"prev_session": prev_session,
|
||||||
|
"context": context,
|
||||||
|
"label": {"target": target},
|
||||||
|
"dimensions": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def combined_label(record: dict[str, Any]) -> str:
|
||||||
|
target = ""
|
||||||
|
label = record.get("label") if isinstance(record.get("label"), dict) else {}
|
||||||
|
if isinstance(label, dict):
|
||||||
|
target = str(label.get("target") or "")
|
||||||
|
dimensions = record.get("dimensions") if isinstance(record.get("dimensions"), dict) else {}
|
||||||
|
complex_value = dimensions.get("complex") if isinstance(dimensions, dict) else None
|
||||||
|
if isinstance(complex_value, bool):
|
||||||
|
return f"complex={'true' if complex_value else 'false'}\n{target}".rstrip()
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def extract_query_from_prompt(prompt: str) -> str:
|
||||||
|
match = re.search(r"\[当前query\]\s*\n用户[::]\s*(.+)", prompt)
|
||||||
|
return match.group(1).strip() if match else ""
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_bool_literal(value: Any) -> str:
|
||||||
|
text = str(value).strip().lower()
|
||||||
|
return "true" if text in {"true", "1", "yes", "y", "是", "复杂"} else "false"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_json_cell(value: Any, default: Any) -> Any:
|
||||||
|
if value in (None, ""):
|
||||||
|
return default
|
||||||
|
if isinstance(value, (dict, list)):
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
return json.loads(str(value))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_runtime_path(path: str) -> Path:
|
||||||
|
raw = Path(path).expanduser()
|
||||||
|
if raw.is_absolute():
|
||||||
|
return raw
|
||||||
|
scratchpad = Path(str(Path.cwd()))
|
||||||
|
if os.environ.get("PYTHON_EXEC_SCRATCHPAD"):
|
||||||
|
scratchpad = Path(os.environ["PYTHON_EXEC_SCRATCHPAD"]).expanduser()
|
||||||
|
parts = raw.parts
|
||||||
|
if parts and parts[0] in {"output", "outputs"}:
|
||||||
|
return scratchpad.parent / "output" / Path(*parts[1:])
|
||||||
|
if parts and parts[0] in {"input", "inputs"}:
|
||||||
|
return scratchpad.parent / "input" / Path(*parts[1:])
|
||||||
|
if parts and parts[0] in {"scratchpad", "scratch"}:
|
||||||
|
return scratchpad / Path(*parts[1:])
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def optional_int(value: Any) -> int | None:
|
||||||
|
if value in (None, ""):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(float(str(value)))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def extract_prediction(decoded: Any, text: str) -> str:
|
||||||
|
if isinstance(decoded, dict):
|
||||||
|
for key in ("generated_text", "text", "output", "response", "result"):
|
||||||
|
value = decoded.get(key)
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.strip()
|
||||||
|
outputs = decoded.get("outputs")
|
||||||
|
if isinstance(outputs, list) and outputs:
|
||||||
|
first = outputs[0]
|
||||||
|
if isinstance(first, str):
|
||||||
|
return first.strip()
|
||||||
|
if isinstance(first, dict):
|
||||||
|
return extract_prediction(first, json.dumps(first, ensure_ascii=False))
|
||||||
|
choices = decoded.get("choices")
|
||||||
|
if isinstance(choices, list) and choices:
|
||||||
|
first = choices[0]
|
||||||
|
if isinstance(first, dict):
|
||||||
|
message = first.get("message")
|
||||||
|
if isinstance(message, dict) and isinstance(message.get("content"), str):
|
||||||
|
return message["content"].strip()
|
||||||
|
if isinstance(first.get("text"), str):
|
||||||
|
return first["text"].strip()
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def try_json(text: str) -> Any:
|
||||||
|
try:
|
||||||
|
return json.loads(text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def preview_samples(samples: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"index": item.get("index"),
|
||||||
|
"query": item.get("query"),
|
||||||
|
"gold_label": item.get("gold_label"),
|
||||||
|
"prompt_preview": str(item.get("prompt") or "")[:200],
|
||||||
|
}
|
||||||
|
for item in samples[:3]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def require_string(payload: dict[str, Any], key: str) -> str:
|
||||||
|
value = payload.get(key)
|
||||||
|
if not isinstance(value, str) or not value.strip():
|
||||||
|
raise LabelingError(f"{key} is required")
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def raise_with_mapping(message: str, columns: list[str] | None = None) -> None:
|
||||||
|
exc = LabelingError(message)
|
||||||
|
exc.extra = {"needs_mapping": True, "columns": columns or []} # type: ignore[attr-defined]
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user