116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
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())
|