mahler
Version:
A automated task composer and HTN based planner for building autonomous system agents
394 lines • 13.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.mermaid = mermaid;
const crypto_1 = require("crypto");
const planner_1 = require("../planner");
const task_1 = require("../task");
const planner_2 = require("../planner");
const assert_1 = require("../assert");
const DAG = require("../dag");
function htmlEncode(s) {
return s.replace(/"/g, () => '"');
}
function hash(o) {
return (0, crypto_1.createHash)('md5')
.update(JSON.stringify(o))
.digest('hex')
.substring(0, 7);
}
function instructionId(s, i) {
if (task_1.Action.is(i)) {
const n = planner_2.PlanAction.from(s, i);
return n.id;
}
return hash({
id: i.id,
path: i.path,
state: s,
target: i.target,
});
}
function fromNode(node, adj) {
if (node == null) {
return;
}
if (planner_2.PlanAction.is(node)) {
// Action nodes always have a parent on the diagram
const parent = adj.get(DiagramNode.fromId(node.id));
return DiagramNode.action(node, parent.id);
}
if (DAG.isFork(node)) {
const actions = node.next.filter(planner_2.PlanAction.is);
if (actions.length > 0) {
const [first] = actions;
// In this case we just use id of the first action in the
// fork as we really should not have multiple fork nodes in the
// diagram pointing to the same actions
return DiagramNode.fromId(`j${first.id.substring(0, 7)}`);
}
const forks = node.next.filter(DAG.isFork);
if (forks.length > 0) {
const ids = forks.map((f) => fromNode(f, adj)).map((n) => n.id);
return DiagramNode.fromId(hash(ids));
}
// An empty fork should never happen
(0, assert_1.assert)(false);
}
}
const DiagramNode = {
fromId(id) {
return {
id,
toString() {
return id;
},
isAction() {
return false;
},
};
},
start() {
return DiagramNode.fromId('start');
},
level(depth) {
return DiagramNode.fromId(`d${depth}`);
},
action(n, parentId) {
return {
id: n.id,
toString() {
// The node representation in the diagram
// is dependent on the parent
return hash({ parent: parentId, action: n.id });
},
isAction() {
return true;
},
};
},
instruction(s, i, parentId) {
if (task_1.Action.is(i)) {
(0, assert_1.assert)(parentId != null);
const n = planner_2.PlanAction.from(s, i);
return DiagramNode.action(n, parentId);
}
return DiagramNode.fromId(instructionId(s, i));
},
fromNode,
stop() {
return DiagramNode.fromId('stop');
},
};
class DiagramAdjacency {
map = new Map();
set(child, parent) {
const p = this.map.get(child.id);
if (p == null) {
this.map.set(child.id, [parent]);
}
else {
p.push(parent);
}
}
has(node) {
return this.map.has(node.id);
}
get(node) {
const p = this.map.get(node.id);
(0, assert_1.assert)(p != null && p.length > 0);
// The official parent of a node is the last parent
// in the list
return p[p.length - 1];
}
getAll(node) {
const p = this.map.get(node.id);
(0, assert_1.assert)(p != null && p.length > 0);
return p;
}
}
/**
* Mermaid classses for nodes
*/
const ERROR_NODE = 'stroke:#f00';
const SELECTED_NODE = 'stroke:#0f0';
class Diagram {
adjacency = new DiagramAdjacency();
parent = null;
depth = 0;
graph = [];
// Keeps track of the index where the method (identified by instruction id)
// was added to the graph to allow backtracking when switching from parallel to
// sequential
callStack = new Map();
meta = {};
constructor() {
this.graph = [];
}
drawJoins(child, prev, first = true) {
if (prev == null || !(DAG.isFork(prev) || planner_2.PlanAction.is(prev))) {
return;
}
const pNode = DiagramNode.fromNode(prev, this.adjacency);
if (this.adjacency.has(child)) {
const list = this.adjacency.getAll(child);
// If there already a link between the child
// and the the previous node then
// we just terminate returning pNode
if (list.findIndex((n) => n.id === pNode.id) >= 0) {
return pNode;
}
}
if (planner_2.PlanAction.is(prev)) {
if (!first) {
this.graph.push(` ${pNode} -.- ${child}`);
this.adjacency.set(child, pNode);
}
return pNode;
}
// If the node is a fork then we need to keep joining up
prev.next.forEach((n) => this.drawJoins(pNode, n, false));
// If we are here, we need to add the new nodes to the graph
if (!first) {
this.graph.push(` ${pNode}(( )) -.- ${child}`);
this.adjacency.set(child, pNode);
}
else {
this.graph.push(` ${pNode}(( ))`);
}
return pNode;
}
drawPlan(node, prev) {
if (node == null) {
// If we reached the end of the plan, add a stop node
this.graph.push(` ${prev} --> ${DiagramNode.stop()}`);
return;
}
if (planner_2.PlanAction.is(node)) {
const parent = this.adjacency.get(DiagramNode.fromId(node.id));
const child = DiagramNode.action(node, parent.id);
this.graph.push(` ${prev} --> ${child}`, ` ${child}:::selected`);
return this.drawPlan(node.next, child);
}
if (DAG.isFork(node)) {
const join = DiagramNode.fromNode(node, this.adjacency);
const fork = DiagramNode.fromId('f' + join.id);
this.graph.push(` ${prev} --> ${fork}(( ))`, ` ${fork}:::selected`);
const ends = node.next.map((n) => this.drawPlan(n, fork));
this.graph.push(` ${join}(( ))`);
ends.forEach(([_, p]) => this.graph.push(` ${p} --> ${join}`));
this.graph.push(` ${join}:::selected`);
const [end] = ends;
(0, assert_1.assert)(end != null);
const [first] = end;
return this.drawPlan(first.next, join);
}
// If the node is an empty node, ignore it
return [node, prev];
}
onStart() {
const node = DiagramNode.start();
this.graph.push('graph TD');
this.graph.push(` ${node}(( ))`);
this.parent = node;
return node;
}
onFindNext(e) {
(0, assert_1.assert)(this.parent != null);
const currNode = DiagramNode.level(e.depth);
// The parent of a next node is either the immediate join after
// joining branches upwards or is the diagram node for the previous
// action or is 'start'
const parentNode = this.drawJoins(currNode, e.prev) ?? DiagramNode.start();
this.graph.push(` ${parentNode} -.- ${currNode}{ }`);
this.adjacency.set(currNode, parentNode);
this.parent = currNode;
this.depth = e.depth;
return currNode;
}
onTryInstruction(e) {
// If this is being called we already had previous events
(0, assert_1.assert)(this.parent != null);
// check if the instruction is an action or a method
// and set the ID and node accordingly
const insId = instructionId(e.state, e.instruction);
// By default we assume the parent node is the level node
let parent = DiagramNode.level(this.depth);
// If that's not the case we need to figure out
// who the right parent is
if (this.parent.id !== parent.id) {
// The current node is either the first child of a compound task
// or there is a previous node in the plan
(0, assert_1.assert)(e.prev != null || e.parent != null);
if (e.parent != null) {
parent = DiagramNode.instruction(e.state, e.parent);
if (e.prev != null) {
const prevNode = DiagramNode.fromNode(e.prev, this.adjacency) ?? parent;
// We go up the adjacency map to find the first action
// node that links to the compound task. If that node is the
// same as the previous node id, then this is the first child
// of the compound task
let p = parent;
while (this.adjacency.has(p) && !this.adjacency.get(p).isAction()) {
p = this.adjacency.get(p);
}
if (!this.adjacency.has(p) ||
this.adjacency.get(p).id !== prevNode.id) {
parent = prevNode;
}
}
else {
// If there are empty nodes before the current action
// on the plan we use those as the parent, otherwise we use
// the already defined parent
parent = this.drawJoins(DiagramNode.fromId(insId), e.prev) ?? parent;
}
}
}
const node = DiagramNode.instruction(e.state, e.instruction, parent.id);
if (task_1.Method.is(e.instruction)) {
this.graph.push(` ${parent} -.- ${node}[["${htmlEncode(e.instruction.description)}"]]`);
this.callStack.set(node.id, this.graph.length - 1);
}
else {
this.graph.push(` ${parent} -.- ${node}("${htmlEncode(e.instruction.description)}")`);
}
this.adjacency.set(node, parent);
this.parent = node;
return node;
}
onBacktracking(e) {
const node = DiagramNode.instruction(e.state, e.method);
const pos = this.callStack.get(node.id);
(0, assert_1.assert)(pos !== undefined);
this.graph.splice(pos + 1);
this.parent = node;
}
onError(e) {
(0, assert_1.assert)(this.parent != null);
const node = DiagramNode.fromId(`${this.parent}-err`);
// Go up the stack to the level the search can continue
if (e === planner_1.SearchFailed) {
if (this.depth > 0) {
this.depth--;
this.parent = DiagramNode.level(this.depth);
}
return node;
}
this.graph.push(` ${this.parent} -.- ${node}[ ]`);
this.graph.push(` ${node}:::error`);
// We reset the parent ID to the current search depth
this.parent = DiagramNode.level(this.depth);
return node;
}
onFound(e) {
const node = DiagramNode.stop();
const parent = this.drawJoins(node, e.prev) ?? DiagramNode.start();
this.graph.push(` ${parent} -.- ${node}(( ))`);
this.graph.push(` ${node}:::finish`);
this.graph.push(` classDef finish stroke:#000,fill:#000`);
}
onSuccess(e) {
const start = DiagramNode.start();
this.graph.push(` ${start}:::selected`);
const n = e.start;
// Draw the plan
this.drawPlan(n, start);
// Add the style data
this.graph.push(` classDef error ${ERROR_NODE}`);
this.graph.push(` classDef selected ${SELECTED_NODE}`);
return;
}
onFailed() {
this.graph.push(` start:::error`);
this.graph.push(` classDef error ${ERROR_NODE}`);
}
updateMeta(node, e) {
this.graph.push(`click ${node} meta "${node}"`);
this.meta[`${node}`] = e;
}
render() {
return this.graph.join('\n');
}
}
/**
* Return a trace function that generates
* a mermaid graph
*/
function mermaid({ meta = false } = {}) {
let diagram = new Diagram();
return Object.assign(function (e) {
// We set the default node to the event type
let node = null;
switch (e.event) {
case 'start':
diagram = new Diagram();
node = diagram.onStart();
break;
case 'find-next':
node = diagram.onFindNext(e);
break;
case 'try-instruction':
node = diagram.onTryInstruction(e);
break;
case 'backtrack-method': {
diagram.onBacktracking(e);
return;
}
case 'found':
diagram.onFound(e);
return;
case 'success':
diagram.onSuccess(e);
return;
case 'failed':
diagram.onFailed();
return;
case 'error':
node = diagram.onError(e);
break;
}
// We add metadata as a clickable event on the node
// we only do this if the meta option is set to avoid storing
// unnecessary data in memory
if (meta && node) {
diagram.updateMeta(node, e);
}
}, {
/**
* Generate a mermaid diagram from the planning trace
*/
render() {
return diagram.render();
},
/**
* Return the metadata associated with the diagram.
*
* The metadata is a record that maps each node to the event
* the node represents.
*/
metadata() {
return diagram.meta;
},
});
}
//# sourceMappingURL=mermaid.js.map