Zion Boggan
repos/TreeTrace/src/adapters/chatgpt.js
zionboggan.com ↗
84 lines · javascript
History for this file →
1
import { newSession, finalizeSession, pushTurn, flattenParts, looksSynthetic, noteAssistantRefusal } from './shared.js';
2
 
3
function conversationList(parsed) {
4
  if (Array.isArray(parsed)) return parsed;
5
  if (parsed && Array.isArray(parsed.conversations)) return parsed.conversations;
6
  if (parsed && parsed.mapping && typeof parsed.mapping === 'object') return [parsed];
7
  return [];
8
}
9
 
10
export function detectChatGPT(parsed) {
11
  const list = conversationList(parsed);
12
  if (!list.length) return false;
13
  const first = list[0];
14
  return Boolean(first && first.mapping && typeof first.mapping === 'object');
15
}
16
 
17
export function parseChatGPT(parsed, path) {
18
  const conversations = conversationList(parsed);
19
  const sessions = [];
20
  for (let i = 0; i < conversations.length; i++) {
21
    const convo = conversations[i];
22
    if (!convo || !convo.mapping) continue;
23
    const session = sessionFromConversation(convo, path, i);
24
    if (session.prompts.length) sessions.push(session);
25
  }
26
  return sessions;
27
}
28
 
29
function sessionFromConversation(convo, path, index) {
30
  const id = convo.conversation_id || convo.id || `chatgpt-${index + 1}`;
31
  const session = newSession(path, id);
32
  if (convo.title) session.title = convo.title;
33
 
34
  const ordered = orderNodes(convo.mapping);
35
  let turn = 0;
36
  for (const node of ordered) {
37
    const msg = node.message;
38
    if (!msg || !msg.author) continue;
39
    const role = msg.author.role;
40
    const text = flattenParts(msg.content && msg.content.parts);
41
    const ts = msg.create_time ? new Date(msg.create_time * 1000).toISOString() : null;
42
 
43
    if (role === 'user') {
44
      if (looksSynthetic(text)) continue;
45
      pushTurn(session, ++turn, text, ts);
46
    } else if (role === 'assistant') {
47
      session.stats.assistantLines++;
48
      if (msg.metadata && msg.metadata.model_slug) session.stats.models.add(msg.metadata.model_slug);
49
      noteAssistantRefusal(session, text);
50
    } else if (role === 'tool') {
51
      session.stats.toolUses++;
52
    }
53
  }
54
  return finalizeSession(session);
55
}
56
 
57
function orderNodes(mapping) {
58
  const nodes = Object.values(mapping).filter((n) => n && n.message);
59
  const withTime = nodes.filter((n) => typeof n.message.create_time === 'number');
60
  if (withTime.length === nodes.length && nodes.length) {
61
    return nodes.slice().sort((a, b) => a.message.create_time - b.message.create_time);
62
  }
63
  return walkFromRoot(mapping);
64
}
65
 
66
function walkFromRoot(mapping) {
67
  let rootId = null;
68
  for (const [id, node] of Object.entries(mapping)) {
69
    if (node && (node.parent === null || node.parent === undefined)) {
70
      rootId = id;
71
      break;
72
    }
73
  }
74
  const out = [];
75
  const seen = new Set();
76
  let cur = rootId;
77
  while (cur && mapping[cur] && !seen.has(cur)) {
78
    seen.add(cur);
79
    out.push(mapping[cur]);
80
    const children = mapping[cur].children || [];
81
    cur = children.length ? children[children.length - 1] : null;
82
  }
83
  return out;
84
}