| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 4fb5313 commit ee623ca
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1503,7 +1503,9 @@ def _log_messages(self, messages, name="message"): | |||
| 1503 | 1503 | """ | |
| 1504 | 1504 | os.makedirs(".cecli/logs/messages", exist_ok=True) | |
| 1505 | 1505 | with open(f".cecli/logs/messages/{name}-{time.time()}.log", "w") as f: | |
| 1506 | - json.dump(messages, f, indent=4, default=lambda o: "<not serializable>") | ||
| 1506 | + json.dump( | ||
| 1507 | + messages, f, indent=4, ensure_ascii=False, default=lambda o: "<not serializable>" | ||
| 1508 | + ) | ||
| 1507 | 1509 | ||
| 1508 | 1510 | def _log_request(self, model_call_dict): | |
| 1509 | 1511 | """ | |
@@ -1513,7 +1515,13 @@ def _log_request(self, model_call_dict): | |||
| 1513 | 1515 | log_file_path = f".cecli/logs/litellm/request-{time.time()}.log" | |
| 1514 | 1516 | ||
| 1515 | 1517 | with open(log_file_path, "a", encoding="utf-8") as f: | |
| 1516 | - json.dump(model_call_dict, f, indent=4, default=lambda o: "<not serializable>") | ||
| 1518 | + json.dump( | ||
| 1519 | + model_call_dict, | ||
| 1520 | + f, | ||
| 1521 | + indent=4, | ||
| 1522 | + ensure_ascii=False, | ||
| 1523 | + default=lambda o: "<not serializable>", | ||
| 1524 | + ) | ||
| 1517 | 1525 | f.write(",\n") | |
| 1518 | 1526 | ||
| 1519 | 1527 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,257 @@ | |||
| 1 | + #!/usr/bin/env python3 | ||
| 2 | + """ | ||
| 3 | + Compare agent trajectory JSON files from .cecli/logs/messages/ | ||
| 4 | + | ||
| 5 | + Sorts them by timestamp and finds where one trajectory diverges | ||
| 6 | + from the next by comparing parsed JSON messages message-by-message. | ||
| 7 | + | ||
| 8 | + Usage: | ||
| 9 | + python scripts/compare-message-history.py | ||
| 10 | + python scripts/compare-message-history.py <directory> | ||
| 11 | + python scripts/compare-message-history.py file1.json file2.json | ||
| 12 | + """ | ||
| 13 | + | ||
| 14 | + import glob | ||
| 15 | + import json | ||
| 16 | + import os | ||
| 17 | + import re | ||
| 18 | + import sys | ||
| 19 | + | ||
| 20 | + DEFAULT_DIR = os.path.join(".cecli", "logs", "messages") | ||
| 21 | + CONTEXT_SIZE = 40 # chars on each side of divergence (80 total window) | ||
| 22 | + | ||
| 23 | + | ||
| 24 | + def _get_timestamp(filepath: str) -> float: | ||
| 25 | + """Extract the timestamp from a message log filename.""" | ||
| 26 | + basename = os.path.basename(filepath) | ||
| 27 | + match = re.match(r"message-(\d+(?:\.\d+)?)\.log", basename) | ||
| 28 | + if match: | ||
| 29 | + return float(match.group(1)) | ||
| 30 | + return 0.0 | ||
| 31 | + | ||
| 32 | + | ||
| 33 | + def _load_messages(filepath: str) -> list[dict]: | ||
| 34 | + """Load and parse a JSON message file.""" | ||
| 35 | + with open(filepath, "r", encoding="utf-8") as f: | ||
| 36 | + data = json.load(f) | ||
| 37 | + if not isinstance(data, list): | ||
| 38 | + print(f"Warning: {filepath} does not contain a JSON array, skipping", file=sys.stderr) | ||
| 39 | + return [] | ||
| 40 | + return data | ||
| 41 | + | ||
| 42 | + | ||
| 43 | + def _serialize_message(msg: dict) -> str: | ||
| 44 | + """Serialize a message dict to a consistent JSON string for comparison.""" | ||
| 45 | + return json.dumps(msg, indent=2, ensure_ascii=False, sort_keys=True) | ||
| 46 | + | ||
| 47 | + | ||
| 48 | + def _compare_messages(msg_a: dict, msg_b: dict) -> tuple[int, int] | None: | ||
| 49 | + """ | ||
| 50 | + Compare two message dicts by serializing to JSON strings. | ||
| 51 | + Returns (divergence_offset, total_diff_bytes) where they differ, or None if identical. | ||
| 52 | + total_diff_bytes is the number of characters in B after the divergence point | ||
| 53 | + (i.e. how much content is different/new). | ||
| 54 | + """ | ||
| 55 | + str_a = _serialize_message(msg_a) | ||
| 56 | + str_b = _serialize_message(msg_b) | ||
| 57 | + for i, (ca, cb) in enumerate(zip(str_a, str_b)): | ||
| 58 | + if ca != cb: | ||
| 59 | + diff_bytes = len(str_b) - i | ||
| 60 | + return i, diff_bytes | ||
| 61 | + if len(str_a) != len(str_b): | ||
| 62 | + diff_bytes = len(str_b) - len(str_a) | ||
| 63 | + return min(len(str_a), len(str_b)), diff_bytes | ||
| 64 | + return None | ||
| 65 | + | ||
| 66 | + | ||
| 67 | + def _get_context_snippet(text: str, pos: int, size: int = CONTEXT_SIZE) -> str: | ||
| 68 | + """Extract an ~80-char window (40 before, 40 after) around a position.""" | ||
| 69 | + start = max(0, pos - size) | ||
| 70 | + end = min(len(text), pos + size) | ||
| 71 | + snippet = text[start:end] | ||
| 72 | + prefix = "" if start == 0 else "..." | ||
| 73 | + suffix = "" if end == len(text) else "..." | ||
| 74 | + return f"{prefix}{snippet}{suffix}" | ||
| 75 | + | ||
| 76 | + """Extract an ~80-char window (40 before, 40 after) around a position.""" | ||
| 77 | + start = max(0, pos - size) | ||
| 78 | + end = min(len(text), pos + size) | ||
| 79 | + snippet = text[start:end] | ||
| 80 | + prefix = "" if start == 0 else "..." | ||
| 81 | + suffix = "" if end == len(text) else "..." | ||
| 82 | + return f"{prefix}{snippet}{suffix}" | ||
| 83 | + | ||
| 84 | + | ||
| 85 | + def compare_files(file_a: str, file_b: str) -> dict | None: | ||
| 86 | + """ | ||
| 87 | + Compare two message log files. | ||
| 88 | + Returns a dict with divergence info, or None if identical. | ||
| 89 | + """ | ||
| 90 | + msgs_a = _load_messages(file_a) | ||
| 91 | + msgs_b = _load_messages(file_b) | ||
| 92 | + | ||
| 93 | + if msgs_a == msgs_b: | ||
| 94 | + return None | ||
| 95 | + | ||
| 96 | + # Pre-compute serialized messages for size calculations | ||
| 97 | + serialized_b = [_serialize_message(m) for m in msgs_b] | ||
| 98 | + | ||
| 99 | + # Compare message by message to find the first differing index | ||
| 100 | + for idx in range(max(len(msgs_a), len(msgs_b))): | ||
| 101 | + if idx >= len(msgs_a): | ||
| 102 | + # File A ran out of messages — divergence is at the start of B's extra message | ||
| 103 | + msg_b_str = serialized_b[idx] | ||
| 104 | + diff_total = len(msg_b_str) + sum(len(s) for s in serialized_b[idx + 1 :]) | ||
| 105 | + return { | ||
| 106 | + "file_a": file_a, | ||
| 107 | + "file_b": file_b, | ||
| 108 | + "message_index": idx, | ||
| 109 | + "offset_in_message": 0, | ||
| 110 | + "diff_bytes_after": diff_total, | ||
| 111 | + "snippet": _get_context_snippet(msg_b_str, 0), | ||
| 112 | + "reason": ( | ||
| 113 | + f"File A has {len(msgs_a)} messages, File B has {len(msgs_b)} (extra message at index {idx})" | ||
| 114 | + ), | ||
| 115 | + "context_b": msg_b_str[:80], | ||
| 116 | + } | ||
| 117 | + if idx >= len(msgs_b): | ||
| 118 | + # File B ran out of messages — divergence is at start of A's extra message | ||
| 119 | + msg_a_str = _serialize_message(msgs_a[idx]) | ||
| 120 | + return { | ||
| 121 | + "file_a": file_a, | ||
| 122 | + "file_b": file_b, | ||
| 123 | + "message_index": idx, | ||
| 124 | + "offset_in_message": 0, | ||
| 125 | + "diff_bytes_after": len(msg_a_str), | ||
| 126 | + "snippet": _get_context_snippet(msg_a_str, 0), | ||
| 127 | + "reason": ( | ||
| 128 | + f"File B has {len(msgs_b)} messages, File A has {len(msgs_a)} (extra message at index {idx})" | ||
| 129 | + ), | ||
| 130 | + "context_b": msg_a_str[:80], | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + # Compare this message's content | ||
| 134 | + result = _compare_messages(msgs_a[idx], msgs_b[idx]) | ||
| 135 | + if result is not None: | ||
| 136 | + offset, _diff_bytes = result | ||
| 137 | + msg_b_str = serialized_b[idx] | ||
| 138 | + # Total diff = remaining of this message + all subsequent messages | ||
| 139 | + diff_total = (len(msg_b_str) - offset) + sum(len(s) for s in serialized_b[idx + 1 :]) | ||
| 140 | + return { | ||
| 141 | + "file_a": file_a, | ||
| 142 | + "file_b": file_b, | ||
| 143 | + "message_index": idx, | ||
| 144 | + "offset_in_message": offset, | ||
| 145 | + "diff_bytes_after": diff_total, | ||
| 146 | + "snippet": _get_context_snippet(msg_b_str, offset), | ||
| 147 | + "reason": f"Messages at index {idx} differ", | ||
| 148 | + "context_b": _get_context_snippet(msg_b_str, offset, 60), | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + return None # Shouldn't reach here | ||
| 152 | + | ||
| 153 | + | ||
| 154 | + def _format_result(result: dict) -> str: | ||
| 155 | + """Format a divergence result as a readable string.""" | ||
| 156 | + lines = [] | ||
| 157 | + lines.append("─" * 80) | ||
| 158 | + lines.append(f"Divergence at message index {result['message_index']}") | ||
| 159 | + lines.append(f" → Offset {result['offset_in_message']} chars into that message (serialized)") | ||
| 160 | + lines.append(f" → ~{result['diff_bytes_after'] / 4} diff tokens after divergence") | ||
| 161 | + lines.append(f" → {result['reason']}") | ||
| 162 | + lines.append(f" → File A: {result['file_a']}") | ||
| 163 | + lines.append(f" → File B: {result['file_b']}") | ||
| 164 | + lines.append("") | ||
| 165 | + lines.append("Context around divergence (from file B serialized):") | ||
| 166 | + lines.append(f" {result['snippet']}") | ||
| 167 | + return "\n".join(lines) | ||
| 168 | + | ||
| 169 | + | ||
| 170 | + def run_all_comparisons(directory: str | None = None) -> None: | ||
| 171 | + """Find all message files, sort by time, and compare adjacent pairs.""" | ||
| 172 | + if directory is None: | ||
| 173 | + directory = DEFAULT_DIR | ||
| 174 | + | ||
| 175 | + pattern = os.path.join(directory, "message-*.log") | ||
| 176 | + files = sorted(glob.glob(pattern), key=_get_timestamp) | ||
| 177 | + | ||
| 178 | + if not files: | ||
| 179 | + print(f"No message files found in {directory}/") | ||
| 180 | + return | ||
| 181 | + | ||
| 182 | + print(f"Found {len(files)} message files in {directory}") | ||
| 183 | + | ||
| 184 | + has_output = False | ||
| 185 | + for i in range(len(files) - 1): | ||
| 186 | + file_a = files[i] | ||
| 187 | + file_b = files[i + 1] | ||
| 188 | + | ||
| 189 | + result = compare_files(file_a, file_b) | ||
| 190 | + if result is None: | ||
| 191 | + print(f"\n{file_a} vs {file_b}") | ||
| 192 | + print(" → Files are identical") | ||
| 193 | + continue | ||
| 194 | + | ||
| 195 | + has_output = True | ||
| 196 | + print(_format_result(result)) | ||
| 197 | + | ||
| 198 | + if not has_output and len(files) >= 2: | ||
| 199 | + print("\nNo divergences found between any adjacent file pairs.") | ||
| 200 | + elif len(files) < 2: | ||
| 201 | + print(f"\nNeed at least 2 files to compare, found {len(files)}.") | ||
| 202 | + | ||
| 203 | + | ||
| 204 | + def run_specific_comparison(file_a: str, file_b: str) -> None: | ||
| 205 | + """Compare two specific files.""" | ||
| 206 | + for fp in (file_a, file_b): | ||
| 207 | + if not os.path.isfile(fp): | ||
| 208 | + print(f"Error: file not found: {fp}", file=sys.stderr) | ||
| 209 | + sys.exit(1) | ||
| 210 | + | ||
| 211 | + result = compare_files(file_a, file_b) | ||
| 212 | + if result is None: | ||
| 213 | + print(f"{file_a} and {file_b} are identical.") | ||
| 214 | + else: | ||
| 215 | + print(_format_result(result)) | ||
| 216 | + | ||
| 217 | + | ||
| 218 | + def main() -> None: | ||
| 219 | + args = sys.argv[1:] | ||
| 220 | + | ||
| 221 | + if not args: | ||
| 222 | + run_all_comparisons() | ||
| 223 | + elif args == ["--all"]: | ||
| 224 | + run_all_comparisons() | ||
| 225 | + elif len(args) == 1: | ||
| 226 | + path = args[0] | ||
| 227 | + if os.path.isdir(path): | ||
| 228 | + run_all_comparisons(path) | ||
| 229 | + elif os.path.isfile(path): | ||
| 230 | + print( | ||
| 231 | + "Error: expected a directory, got a file. Use two arguments to compare specific files.", | ||
| 232 | + file=sys.stderr, | ||
| 233 | + ) | ||
| 234 | + sys.exit(1) | ||
| 235 | + else: | ||
| 236 | + print(f"Error: path not found: {path}", file=sys.stderr) | ||
| 237 | + sys.exit(1) | ||
| 238 | + elif len(args) == 2: | ||
| 239 | + run_specific_comparison(args[0], args[1]) | ||
| 240 | + else: | ||
| 241 | + print("Usage:", file=sys.stderr) | ||
| 242 | + print( | ||
| 243 | + f" {sys.argv[0]} # Compare all files in default directory ({DEFAULT_DIR})", | ||
| 244 | + file=sys.stderr, | ||
| 245 | + ) | ||
| 246 | + print( | ||
| 247 | + f" {sys.argv[0]} <directory> # Compare all files in the given directory", | ||
| 248 | + file=sys.stderr, | ||
| 249 | + ) | ||
| 250 | + print( | ||
| 251 | + f" {sys.argv[0]} file1.json file2.json # Compare two specific files", file=sys.stderr | ||
| 252 | + ) | ||
| 253 | + sys.exit(1) | ||
| 254 | + | ||
| 255 | + | ||
| 256 | + if __name__ == "__main__": | ||
| 257 | + main() | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments