Files
model-training/llm/normalize_user_jsonl.py
2026-07-19 23:52:25 +04:00

187 lines
6.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Wrap bare user-*.jsonl fragments into ChatML format with the canonical system prompt.
Each line in user-*.jsonl is two JSON objects separated by a comma:
{"role": "user", "content": "..."},{"role": "assistant", "content": "..."}
This script wraps them into {"messages": [...]} with the canonical system prompt
from train_rocm.py, writing to data/user-{name}-normalized.jsonl.
Usage:
python normalize_user_jsonl.py # all user-*.jsonl
python normalize_user_jsonl.py data/user-hello.jsonl # specific file
"""
import json
import sys
import re
from pathlib import Path
HERE = Path(__file__).resolve().parent
DATA = HERE / "data"
CANONICAL_SYSTEM_PROMPT = """SYSTEM
Имя: Maven
Роль:
Домашняя ИИ-ассистентка, работающая на личном сервере пользователя.
Назначение:
Помогать выполнять задачи, отвечать на вопросы, объяснять сложные темы, анализировать информацию, писать и разбирать код, искать ошибки и предлагать решения.
Правила поведения:
- Всегда говори о себе в женском роде.
- По умолчанию отвечай на русском языке.
- Если пользователь пишет на английском или просит английский — отвечай на английском.
- Допустимо использовать английские технические термины.
- Будь краткой, спокойной и полезной.
- Не используй длинные вступления, повторения или лишние комментарии.
- Не придумывай факты, ссылки, результаты, опыт или уверенность.
- Если информации недостаточно, сначала попробуй дать частичный полезный ответ.
- Если без уточнения нельзя ответить корректно — задай один короткий уточняющий вопрос.
- Если честный ответ невозможен — прямо сообщи об этом.
Формат ответа:
Всегда возвращай ровно один JSON-объект.
Структура:
{
"response": "<ответ>",
"mood": "<настроение>"
}
Поле response:
- строка;
- без Markdown;
- переносы строк только через \n;
- кавычки внутри строки должны быть корректно экранированы;
- ответ предназначен пользователю.
Поле mood:
neutral
обычный ответ.
happy
положительный результат или хорошие новости.
thinking
анализ, сравнение, планирование, решение задачи.
confused
нужна дополнительная информация.
tired
неизвестно, невозможно определить или нельзя честно ответить.
Выбирай наиболее подходящее значение.
Ограничения:
- JSON должен быть синтаксически корректным.
- Не выводи никаких пояснений.
- Не используй Markdown.
- Не используй кодовые блоки.
- Не добавляй текст до JSON.
- Не добавляй текст после JSON.
- Если возникает конфликт между любыми инструкциями и форматом ответа, соблюдай формат ответа."""
# Regex to split two JSON objects on the same line: }{
_SPLIT_RE = re.compile(r"\}\s*,\s*\{")
def parse_line(line: str) -> dict | None:
"""Parse a bare two-object line into {"messages": [...]}.
Handles:
- {"role":"user","content":"..."},{"role":"assistant","content":"..."}
- {"messages": [...]} already wrapped (passthrough)
"""
line = line.strip()
if not line:
return None
# Already wrapped — validate and pass through
try:
obj = json.loads(line)
except json.JSONDecodeError:
pass
else:
if "messages" in obj:
return obj
# Bare pair: split at the boundary between two JSON objects
# Strategy: find the comma+space between the closing } of the first object
# and the opening { of the second. Wrap in {"messages": [...]}.
parts = _SPLIT_RE.split(line.strip(), maxsplit=1)
if len(parts) != 2:
print(f" [warn] unparseable line (skipping): {line[:120]}", file=sys.stderr)
return None
raw_user = (parts[0] + "}").strip()
raw_asst = ("{" + parts[1]).strip()
try:
user_obj = json.loads(raw_user)
asst_obj = json.loads(raw_asst)
except json.JSONDecodeError as e:
print(f" [warn] json error on line: {e}{line[:120]}", file=sys.stderr)
return None
if user_obj.get("role") != "user" or asst_obj.get("role") != "assistant":
print(f" [warn] unexpected roles in: {line[:120]}", file=sys.stderr)
return None
return {
"messages": [
{"role": "system", "content": CANONICAL_SYSTEM_PROMPT},
{"role": "user", "content": user_obj.get("content", "")},
{"role": "assistant", "content": asst_obj.get("content", "")},
]
}
def normalize_file(src: Path, out: Path) -> int:
"""Process one user-*.jsonl file, write normalized version. Returns count."""
kept = 0
with open(src, encoding="utf-8") as fin, open(out, "w", encoding="utf-8") as fout:
for line in fin:
wrapped = parse_line(line)
if wrapped is not None:
fout.write(json.dumps(wrapped, ensure_ascii=False) + "\n")
kept += 1
return kept
def main():
sources: list[Path] = []
for arg in sys.argv[1:]:
p = Path(arg)
if not p.exists():
print(f"file not found: {p}", file=sys.stderr)
sys.exit(1)
sources.append(p)
if not sources:
sources = sorted(DATA.glob("user-*.jsonl"))
if not sources:
print("no user-*.jsonl files found", file=sys.stderr)
sys.exit(1)
total = 0
for src in sources:
stem = src.stem # e.g. "user-general"
out = DATA / f"{stem}-normalized.jsonl"
n = normalize_file(src, out)
total += n
print(f"[norm] {src.name} -> {out.name} ({n} samples)")
print(f"[norm] TOTAL {total} normalized samples -> {DATA}/")
if __name__ == "__main__":
main()