Spaces:
Sleeping
Sleeping
File size: 4,207 Bytes
7334a07 | 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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | /**
* Tree manipulation utilities for the conversation graph.
* Nodes are stored as a flat map: { [nodeId]: ConversationNode }
*/
/**
* Get the path from root to a specific node (inclusive).
* Returns an array of nodeIds ordered root → target.
*/
export function getAncestryPath(nodes, nodeId) {
const path = [];
let current = nodeId;
while (current) {
path.unshift(current);
const node = nodes[current];
if (!node) break;
current = node.parentId;
}
return path;
}
/**
* Get direct children of a node.
*/
export function getChildren(nodes, nodeId) {
return Object.values(nodes).filter((n) => n.parentId === nodeId);
}
/**
* Get all descendant nodeIds (BFS order).
*/
export function getDescendants(nodes, nodeId) {
const descendants = [];
const queue = [nodeId];
while (queue.length > 0) {
const current = queue.shift();
const children = getChildren(nodes, current);
for (const child of children) {
descendants.push(child.id);
queue.push(child.id);
}
}
return descendants;
}
/**
* Get the deepest leaf node in a subtree, following the first child at each level.
* Used for finding the "default" branch path.
*/
export function getDeepestLeaf(nodes, nodeId) {
let current = nodeId;
while (true) {
const children = getChildren(nodes, current);
if (children.length === 0) return current;
// Follow the first (oldest) child
children.sort((a, b) => a.timestamp - b.timestamp);
current = children[0].id;
}
}
/**
* Build the messages array for LLM context from root to a specific node.
* Returns an array of { role, content } messages.
*/
export function buildConversationHistory(nodes, nodeId) {
const path = getAncestryPath(nodes, nodeId);
const messages = [
{
role: 'system',
content:
'You are a helpful, concise assistant. Keep responses focused and well-structured.',
},
];
for (const id of path) {
const node = nodes[id];
if (!node) continue;
if (node.userMessage) {
messages.push({ role: 'user', content: node.userMessage });
}
if (node.assistantMessage) {
messages.push({ role: 'assistant', content: node.assistantMessage });
}
}
return messages;
}
/**
* Build messages for generating a response at a specific node.
* Includes all parent context but only the user message of the target node.
*/
export function buildGenerationContext(nodes, nodeId) {
const path = getAncestryPath(nodes, nodeId);
const messages = [
{
role: 'system',
content:
'You are a helpful, concise assistant. Keep responses focused and well-structured.',
},
];
for (let i = 0; i < path.length; i++) {
const node = nodes[path[i]];
if (!node) continue;
if (node.userMessage) {
messages.push({ role: 'user', content: node.userMessage });
}
// Only include assistant message for nodes before the target
if (i < path.length - 1 && node.assistantMessage) {
messages.push({ role: 'assistant', content: node.assistantMessage });
}
}
return messages;
}
/**
* Convert the conversation tree into react-force-graph data format.
*/
export function toGraphData(nodes, mainBranchPath = [], activeBranchPath = []) {
const mainSet = new Set(mainBranchPath);
const activeSet = new Set(activeBranchPath);
const graphNodes = Object.values(nodes).map((node) => ({
id: node.id,
name: node.userMessage
? node.userMessage.slice(0, 60) + (node.userMessage.length > 60 ? '…' : '')
: 'Start',
userMessage: node.userMessage || '',
assistantMessage: node.assistantMessage || '',
status: node.status,
isMain: mainSet.has(node.id),
isActive: activeSet.has(node.id),
isRoot: !node.parentId,
childCount: getChildren(nodes, node.id).length,
depth: getAncestryPath(nodes, node.id).length - 1,
}));
const graphLinks = Object.values(nodes)
.filter((n) => n.parentId)
.map((n) => ({
source: n.parentId,
target: n.id,
isMain: mainSet.has(n.parentId) && mainSet.has(n.id),
isActive: activeSet.has(n.parentId) && activeSet.has(n.id),
}));
return { nodes: graphNodes, links: graphLinks };
}
|