q
File size: 4,173 Bytes
3365e13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// core/schema.js — lean, dependency-free validators ("zod-lite") for the LibreChat-faithful
// Conversation / Message / Preset shapes. Field names mirror LibreChat's data model (packages/
// data-schemas) so an exported conversation interoperates; values are validated/normalised, not
// hashed (the κ-object store is what makes them content-addressed). No build step, no dep.

const isStr = (v) => typeof v === "string";
const isBool = (v) => typeof v === "boolean";
const isNum = (v) => typeof v === "number" && !Number.isNaN(v);
const arr = (v) => (Array.isArray(v) ? v : []);
const clampNum = (v, lo, hi, d) => (isNum(v) ? Math.min(hi, Math.max(lo, v)) : d);

// A chat message (one node in the parentMessageId tree).
export function normMessage(m = {}) {
  return {
    messageId: isStr(m.messageId) ? m.messageId : null,
    conversationId: isStr(m.conversationId) ? m.conversationId : null,
    parentMessageId: isStr(m.parentMessageId) ? m.parentMessageId : null,   // tree edge
    sender: isStr(m.sender) ? m.sender : (m.isCreatedByUser ? "User" : "Assistant"),
    isCreatedByUser: isBool(m.isCreatedByUser) ? m.isCreatedByUser : false,
    model: isStr(m.model) ? m.model : null,
    text: isStr(m.text) ? m.text : "",
    content: arr(m.content),                          // structured parts (text/tool/image/reasoning)
    tokenCount: isNum(m.tokenCount) ? m.tokenCount : null,
    error: m.error || null,
    unfinished: !!m.unfinished,
    feedback: m.feedback && isStr(m.feedback.rating || "") ? { rating: m.feedback.rating, tag: m.feedback.tag || null, text: m.feedback.text || "" } : (m.feedback || null),
    files: arr(m.files),
    createdAt: isStr(m.createdAt) ? m.createdAt : null,
    receiptKappa: isStr(m.receiptKappa) ? m.receiptKappa : null,             // links the PROV-O inference receipt
  };
}

// A conversation (spreads the active preset, like LibreChat's conversationPreset).
export function normConversation(c = {}) {
  return {
    conversationId: isStr(c.conversationId) ? c.conversationId : null,
    title: isStr(c.title) ? c.title : "New Chat",
    tags: arr(c.tags),
    files: arr(c.files),
    folder: isStr(c.folder) ? c.folder : null,
    archived: !!c.archived,
    favorite: !!c.favorite,
    isTemporary: !!c.isTemporary,
    preset: c.preset ? normPreset(c.preset) : null,
    createdAt: isStr(c.createdAt) ? c.createdAt : null,
    updatedAt: isStr(c.updatedAt) ? c.updatedAt : null,
  };
}

// A preset / parameter bundle. Bounds mirror common LLM ranges; model is the local κ-object id.
export function normPreset(p = {}) {
  return {
    presetId: isStr(p.presetId) ? p.presetId : null,
    title: isStr(p.title) ? p.title : "Preset",
    model: isStr(p.model) ? p.model : null,
    temperature: clampNum(p.temperature, 0, 2, 0),
    top_p: clampNum(p.top_p, 0, 1, 1),
    top_k: clampNum(p.top_k, 0, 200, 40),
    maxOutputTokens: clampNum(p.maxOutputTokens, 1, 8192, 1024),
    promptPrefix: isStr(p.promptPrefix) ? p.promptPrefix : "",
    presence_penalty: clampNum(p.presence_penalty, -2, 2, 0),
    frequency_penalty: clampNum(p.frequency_penalty, -2, 2, 0),
    stop: arr(p.stop),
    iconURL: isStr(p.iconURL) ? p.iconURL : null,
    isArchived: !!p.isArchived,
  };
}

// Walk a flat message list into the LibreChat parentMessageId tree → {roots, byId, childrenOf}.
export function buildTree(messages) {
  const byId = new Map(); const childrenOf = new Map(); const roots = [];
  for (const m of messages) { byId.set(m.messageId, m); childrenOf.set(m.messageId, []); }
  for (const m of messages) {
    if (m.parentMessageId && childrenOf.has(m.parentMessageId)) childrenOf.get(m.parentMessageId).push(m.messageId);
    else roots.push(m.messageId);
  }
  return { roots, byId, childrenOf };
}

// The active thread = follow the chosen child at each branch from a root to a leaf.
export function threadOf(tree, { choose = (siblings) => siblings[siblings.length - 1] } = {}) {
  const out = []; let cur = tree.roots.length ? choose(tree.roots) : null;
  while (cur) { out.push(tree.byId.get(cur)); const kids = tree.childrenOf.get(cur) || []; cur = kids.length ? choose(kids) : null; }
  return out;
}