from __future__ import annotations import argparse import json import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DEFAULT_INPUT = ROOT / "plugins" / "1c" / "training" / "examples" / "instruction.examples.jsonl" SECRET_PATTERNS = { "password assignment": re.compile(r"(?i)(password|passwd|пароль)\s*[:=]\s*\S+"), "token assignment": re.compile(r"(?i)(token|api[_-]?key|secret|ключ)\s*[:=]\s*\S+"), "connection string": re.compile(r"(?i)(server|host|database|uid|user id|pwd)\s*="), "private key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), } VALID_ROLES = {"system", "user", "assistant", "tool"} def scan_secrets(text: str) -> list[str]: return [name for name, pattern in SECRET_PATTERNS.items() if pattern.search(text)] def iter_jsonl(path: Path) -> list[tuple[int, dict]]: records: list[tuple[int, dict]] = [] with path.open("r", encoding="utf-8") as handle: for line_number, line in enumerate(handle, start=1): line = line.strip() if not line: continue try: data = json.loads(line) except json.JSONDecodeError as exc: raise ValueError(f"{path}:{line_number}: invalid JSON: {exc}") from exc if not isinstance(data, dict): raise ValueError(f"{path}:{line_number}: record must be an object") records.append((line_number, data)) return records def validate_record(path: Path, line_number: int, record: dict) -> list[str]: errors: list[str] = [] prefix = f"{path}:{line_number}" if not record.get("id"): errors.append(f"{prefix}: id is required") messages = record.get("messages") if not isinstance(messages, list) or len(messages) < 2: errors.append(f"{prefix}: messages must contain at least 2 items") return errors has_user = False has_assistant = False for index, message in enumerate(messages): msg_prefix = f"{prefix}: messages[{index}]" if not isinstance(message, dict): errors.append(f"{msg_prefix}: message must be an object") continue role = message.get("role") content = message.get("content") if role not in VALID_ROLES: errors.append(f"{msg_prefix}: invalid role `{role}`") if not isinstance(content, str) or not content.strip(): errors.append(f"{msg_prefix}: content is required") continue if role == "user": has_user = True if role == "assistant": has_assistant = True matches = scan_secrets(content) for match in matches: errors.append(f"{msg_prefix}: possible secret detected: {match}") if not has_user: errors.append(f"{prefix}: at least one user message is required") if not has_assistant: errors.append(f"{prefix}: at least one assistant message is required") return errors def main() -> int: parser = argparse.ArgumentParser(description="Validate 1C training JSONL data.") parser.add_argument("path", nargs="?", type=Path, default=DEFAULT_INPUT) args = parser.parse_args() errors: list[str] = [] try: records = iter_jsonl(args.path) except Exception as exc: print(f"Failed to read training data: {exc}", file=sys.stderr) return 1 for line_number, record in records: errors.extend(validate_record(args.path, line_number, record)) if errors: print("1C training data validation failed:", file=sys.stderr) for error in errors: print(f"- {error}", file=sys.stderr) return 1 print(f"Validated {len(records)} 1C training record(s): {args.path}") return 0 if __name__ == "__main__": raise SystemExit(main())