| import json |
| from dataclasses import asdict, is_dataclass |
| from datetime import UTC, datetime |
| from pathlib import Path |
|
|
| from utils.helpers import slugify |
|
|
|
|
| def _serialise_message(message: dict[str, object]) -> dict[str, object]: |
| payload: dict[str, object] = { |
| "role": message["role"], |
| "content": message["content"], |
| } |
| contrast = message.get("_contrast") |
| if contrast is not None: |
| payload["_contrast"] = asdict(contrast) if is_dataclass(contrast) else contrast |
| return payload |
|
|
|
|
| def save_chat_export( |
| *, |
| model_name: str, |
| dataset_source: str, |
| persona_id: str, |
| persona_name: str | None, |
| prompt_mode: str, |
| system_prompt: str | None, |
| messages: list[dict[str, str]], |
| generation: dict[str, object], |
| panel_label: str | None = None, |
| ) -> Path: |
| """Save the current chat session to ``artifacts/chats`` as JSON. |
| |
| Args: |
| model_name: Model identifier used for the chat. |
| dataset_source: Human-readable dataset source label. |
| persona_id: Selected persona id. |
| persona_name: Selected persona display name, if available. |
| prompt_mode: Active system prompt mode. |
| system_prompt: Current system prompt text, if any. |
| messages: Conversation messages without the system prompt. |
| generation: Generation settings used for the chat. |
| panel_label: Optional side label (e.g. "left"/"right") for compare-mode exports. |
| |
| Returns: |
| The path the export was written to. |
| """ |
|
|
| from persona_data.environment import get_artifacts_dir |
|
|
| payload = { |
| "model_name": model_name, |
| "dataset_source": dataset_source, |
| "persona": { |
| "id": persona_id, |
| "name": persona_name, |
| }, |
| "panel_label": panel_label, |
| "prompt_mode": prompt_mode, |
| "generation": generation, |
| "messages": ( |
| [{"role": "system", "content": system_prompt}] if system_prompt else [] |
| ) |
| + [_serialise_message(message) for message in messages], |
| } |
|
|
| export_dir = ( |
| get_artifacts_dir() |
| / "chats" |
| / "__".join(slugify(part) for part in model_name.split("/")) |
| / slugify(persona_id) |
| ) |
| export_dir.mkdir(parents=True, exist_ok=True) |
|
|
| timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ") |
| filename_parts = [ |
| timestamp, |
| slugify(persona_name or persona_id), |
| slugify(prompt_mode), |
| ] |
| if panel_label: |
| filename_parts.append(slugify(panel_label)) |
| export_path = export_dir / f"{'__'.join(filename_parts)}.json" |
| export_path.write_text( |
| f"{json.dumps(payload, indent=2, ensure_ascii=False)}\n", |
| encoding="utf-8", |
| ) |
|
|
| return export_path |
|
|