from __future__ import annotations import json from dataclasses import asdict, dataclass from math import ceil from pathlib import Path BLOCK_TOKEN_BUDGET = 24 @dataclass(frozen=True) class SmallAppendTraceConfig: output_path: Path session_count: int = 8 turns_per_session: int = 3 initial_input_length: int = 10_000 append_input_length: int = 1_000 output_length: int = 1_000 inter_turn_gap_s: float = 1.0 session_stagger_s: float = 0.1 request_type: str = "coder" @dataclass(frozen=True) class SmallAppendTraceSummary: output_path: str session_count: int turns_per_session: int request_count: int initial_input_length: int append_input_length: int output_length: int inter_turn_gap_s: float session_stagger_s: float def write_small_append_trace(config: SmallAppendTraceConfig) -> SmallAppendTraceSummary: if config.session_count <= 0: raise ValueError("session_count must be > 0") if config.turns_per_session <= 0: raise ValueError("turns_per_session must be > 0") if config.initial_input_length < 0: raise ValueError("initial_input_length must be >= 0") if config.append_input_length < 0: raise ValueError("append_input_length must be >= 0") if config.output_length < 0: raise ValueError("output_length must be >= 0") config.output_path.parent.mkdir(parents=True, exist_ok=True) records: list[dict[str, object]] = [] next_chat_id = 1_000_000 for session_idx in range(config.session_count): root_chat_id = next_chat_id previous_chat_id = -1 session_base_time = session_idx * config.session_stagger_s base_block_count = ceil(config.initial_input_length / BLOCK_TOKEN_BUDGET) base_hash_ids = [ _hash_id_for(session_idx=session_idx, block_idx=block_idx) for block_idx in range(base_block_count) ] for turn_idx in range(config.turns_per_session): chat_id = root_chat_id if turn_idx == 0 else next_chat_id if turn_idx > 0: next_chat_id += 1 input_length = config.initial_input_length + turn_idx * ( config.append_input_length + config.output_length ) total_block_count = ceil(input_length / BLOCK_TOKEN_BUDGET) hash_ids = base_hash_ids + [ _hash_id_for( session_idx=session_idx, block_idx=base_block_count + append_block_idx, ) for append_block_idx in range(max(0, total_block_count - base_block_count)) ] records.append( { "chat_id": chat_id, "parent_chat_id": previous_chat_id, "timestamp": session_base_time + turn_idx * config.inter_turn_gap_s, "input_length": input_length, "output_length": config.output_length, "type": config.request_type, "turn": turn_idx + 1, "hash_ids": hash_ids, } ) previous_chat_id = chat_id next_chat_id += 1 records.sort(key=lambda item: float(item["timestamp"])) with config.output_path.open("w", encoding="utf-8") as handle: for record in records: handle.write(json.dumps(record, sort_keys=True) + "\n") summary = SmallAppendTraceSummary( output_path=str(config.output_path), session_count=config.session_count, turns_per_session=config.turns_per_session, request_count=len(records), initial_input_length=config.initial_input_length, append_input_length=config.append_input_length, output_length=config.output_length, inter_turn_gap_s=config.inter_turn_gap_s, session_stagger_s=config.session_stagger_s, ) summary_path = config.output_path.with_suffix( config.output_path.suffix + ".summary.json" ) with summary_path.open("w", encoding="utf-8") as handle: json.dump(asdict(summary), handle, indent=2, sort_keys=True) return summary def _hash_id_for(*, session_idx: int, block_idx: int) -> int: return session_idx * 1_000_000 + block_idx