"""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 (pass‑through) """ 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()