Files
DemonGatanjieu-Anomalous_Mo…/tests/graph_splice.mjs
T
DemonGatanjieuandClaude Opus 5.5 cb523930da Guide the AI with fewer detours and guesses
- An AI app calling by an older tool list (tool_help or use_tool not offered,
  use_tool for a tool offered directly, a tool the list lacks) is told so, and
  the task header shows how to reconnect until the app fetches tools/list again.
- add_lora / set_model compare the base models on the model line
  (model_advice.py; the page reports the line) and add base_model_note.
- get_model gives author_examples (sampler, steps, CFG, size, a LoRA's weight
  from the Civitai record's example pictures) and the version name.
- run_workflow says ComfyUI's refusal as which node, what is wrong and the tool
  that fixes it (run_refusal.py; mcp_run.js passes the refusal as data).
- Gates "When I don't answer": wait for me, or go with the recommendation once
  the waiting time runs out (choices with a recommended option only), handed to
  the AI in the same call and shown on the stage and under Decisions.
- A tool budget test (count, size, description length, required arguments) and
  the exam tasks with their scorecard (docs/architecture/tool-exam.md,
  tools/tool_exam.mjs). The tool set's text recommends Full for Antigravity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:26:41 +08:00

141 lines
7.0 KiB
JavaScript

// Where a LoRA goes and how it is wired (web/modules/graph_splice.js), on a small stand-in graph.
import assert from 'node:assert/strict';
import { chainEnd, loraChain, loraInsertionCapabilities, mainModelLoaders, modelLineStart, planLoraInsertion, spliceLora } from '../web/modules/graph_splice.js';
const SLOTS = {
CheckpointLoaderSimple: [[], ['MODEL', 'CLIP', 'VAE']],
UNETLoader: [[], ['MODEL']],
CLIPLoader: [[], ['CLIP']],
CLIPTextEncode: [['CLIP'], ['CONDITIONING']],
KSampler: [['MODEL', 'CONDITIONING', 'CONDITIONING', 'LATENT'], ['LATENT']],
LoraLoader: [['MODEL', 'CLIP'], ['MODEL', 'CLIP']],
LoraLoaderModelOnly: [['MODEL'], ['MODEL']],
};
class Graph {
constructor() { this._nodes = []; this.links = {}; this.nextLink = 1; this.nextNode = 1; }
getNodeById(id) { return this._nodes.find(node => node.id === id) || null; }
getLink(id) { return this.links[id] || null; }
add(node) { node.id ??= this.nextNode++; node.graph = this; this._nodes.push(node); }
remove(node) {
node.inputs.forEach((input, slot) => input.link != null && this.unlink(input.link));
node.outputs.forEach(output => [...output.links].forEach(id => this.unlink(id)));
this._nodes = this._nodes.filter(item => item !== node);
}
unlink(id) {
const link = this.links[id];
if (!link) return;
const origin = this.getNodeById(link.origin_id);
origin.outputs[link.origin_slot].links = origin.outputs[link.origin_slot].links.filter(item => item !== id);
this.getNodeById(link.target_id).inputs[link.target_slot].link = null;
delete this.links[id];
}
}
function node(graph, type, failConnect = false) {
const [inputs, outputs] = SLOTS[type];
const item = {
type, mode: 0,
inputs: inputs.map(t => ({ type: t, link: null })),
outputs: outputs.map(t => ({ type: t, links: [] })),
connect(slot, target, targetSlot) {
if (failConnect) return null;
const g = this.graph;
if (target.inputs[targetSlot].link != null) g.unlink(target.inputs[targetSlot].link);
const id = g.nextLink++;
g.links[id] = { id, origin_id: this.id, origin_slot: slot, target_id: target.id, target_slot: targetSlot };
this.outputs[slot].links.push(id);
target.inputs[targetSlot].link = id;
return g.links[id];
},
};
if (graph) graph.add(item);
return item;
}
const source = (graph, target, slot) => {
const link = graph.getLink(target.inputs[slot].link);
return link ? `${graph.getNodeById(link.origin_id).type}#${link.origin_id}` : null;
};
// A checkpoint whose CLIP feeds both prompts.
function checkpointWorkflow() {
const g = new Graph();
const ckpt = node(g, 'CheckpointLoaderSimple');
const pos = node(g, 'CLIPTextEncode');
const neg = node(g, 'CLIPTextEncode');
const sampler = node(g, 'KSampler');
ckpt.connect(1, pos, 0); ckpt.connect(1, neg, 0); ckpt.connect(0, sampler, 0);
pos.connect(0, sampler, 1); neg.connect(0, sampler, 2);
return { g, ckpt, pos, neg, sampler };
}
{
const { g, ckpt, pos, neg, sampler } = checkpointWorkflow();
const plan = planLoraInsertion(g, ckpt, 'after', { textEncoder: true });
assert.equal(plan.clip, true);
const lora = spliceLora({ graph: g, plan, anchorNode: ckpt, insertedNode: node(null, 'LoraLoader') });
assert.equal(source(g, lora, 0), `CheckpointLoaderSimple#${ckpt.id}`);
assert.equal(source(g, lora, 1), `CheckpointLoaderSimple#${ckpt.id}`);
for (const prompt of [pos, neg]) assert.equal(source(g, prompt, 0), `LoraLoader#${lora.id}`, 'both prompts move behind the LoRA');
assert.equal(source(g, sampler, 0), `LoraLoader#${lora.id}`);
// The next one goes after the LoRA already there; a model-only LoRA leaves CLIP alone.
assert.equal(chainEnd(g, mainModelLoaders(g)[0], 'MODEL'), lora);
const plan2 = planLoraInsertion(g, lora, 'after', { textEncoder: false });
const lora2 = spliceLora({ graph: g, plan: plan2, anchorNode: lora, insertedNode: node(null, 'LoraLoaderModelOnly') });
assert.equal(source(g, sampler, 0), `LoraLoaderModelOnly#${lora2.id}`);
assert.equal(source(g, pos, 0), `LoraLoader#${lora.id}`);
// The model line, for the server's base-model check: the loader it starts at and its LoRAs in order.
assert.deepEqual(loraChain(g, ckpt, 'MODEL'), [lora, lora2]);
assert.equal(modelLineStart(g, lora2), ckpt);
assert.equal(modelLineStart(g, sampler), ckpt);
assert.equal(modelLineStart(g, ckpt), ckpt, 'a loader starts its own line');
}
{
// A UNet with its own CLIP loader: the CLIP line comes from the CLIP loader.
const g = new Graph();
const unet = node(g, 'UNETLoader');
const clip = node(g, 'CLIPLoader');
const pos = node(g, 'CLIPTextEncode');
const neg = node(g, 'CLIPTextEncode');
const sampler = node(g, 'KSampler');
clip.connect(0, pos, 0); clip.connect(0, neg, 0); unet.connect(0, sampler, 0);
pos.connect(0, sampler, 1); neg.connect(0, sampler, 2);
const plan = planLoraInsertion(g, unet, 'after', { textEncoder: true });
const lora = spliceLora({ graph: g, plan, anchorNode: unet, insertedNode: node(null, 'LoraLoader') });
assert.equal(source(g, lora, 0), `UNETLoader#${unet.id}`);
assert.equal(source(g, lora, 1), `CLIPLoader#${clip.id}`);
assert.equal(source(g, neg, 0), `LoraLoader#${lora.id}`);
assert.equal(planLoraInsertion(g, unet, 'after', { textEncoder: false }).clip, false);
}
{
// Before a sampler: only its own model input moves; CLIP through the prompts wired into it.
const { g, ckpt, pos, neg, sampler } = checkpointWorkflow();
const caps = loraInsertionCapabilities(g, sampler);
assert.equal(caps.before.supported, true);
assert.equal(caps.after.supported, false); // a sampler has no MODEL output
const plan = planLoraInsertion(g, sampler, 'before', { textEncoder: true });
assert.equal(plan.clip, true);
const lora = spliceLora({ graph: g, plan, anchorNode: sampler, insertedNode: node(null, 'LoraLoader') });
assert.equal(source(g, sampler, 0), `LoraLoader#${lora.id}`);
assert.equal(source(g, pos, 0), `LoraLoader#${lora.id}`);
assert.equal(source(g, neg, 0), `LoraLoader#${lora.id}`);
assert.equal(source(g, lora, 1), `CheckpointLoaderSimple#${ckpt.id}`);
}
{
// A connection that fails leaves the graph as it was.
const { g, ckpt, pos, sampler } = checkpointWorkflow();
const plan = planLoraInsertion(g, ckpt, 'after', { textEncoder: true });
const broken = node(null, 'LoraLoader', true);
assert.throws(() => spliceLora({ graph: g, plan, anchorNode: ckpt, insertedNode: broken }), /Failed to connect/);
assert.equal(g._nodes.includes(broken), false);
assert.equal(source(g, pos, 0), `CheckpointLoaderSimple#${ckpt.id}`);
assert.equal(source(g, sampler, 0), `CheckpointLoaderSimple#${ckpt.id}`);
assert.equal(planLoraInsertion(g, pos, 'after').supported, false); // a prompt has no MODEL output
}
console.log('graph_splice: ok');