Shorten the daily tools' descriptions and begin_task's rules

The descriptions of the daily set are rewritten without the repeats and
roundabout phrasing. Their rules and don'ts stay: no cropping or editing
pictures with code, no reading ComfyUI's folders, ask the user in the
workbench. So do the shared property texts: the query words, node ids, the
task id. tools/list for the daily set drops from about 27,300 to 22,200
characters, and the full one from 46,000 to 40,700. begin_task's note, read at
the start of every task, drops from about 1,900 to 900 characters. The
replacements were made with ast, so each description's adjacent literals
changed as one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-10-24 15:54:42 +08:00
co-authored by Claude Opus 5.5
parent 6246df480d
commit f7d8a69e75
8 changed files with 102 additions and 149 deletions
+4 -2
View File
@@ -15,6 +15,8 @@
- The answer that first tells the AI its run ended, run_status or any other, carries up to four thumbnails of the run's pictures, once per run, so it need not fetch them with get_image_info.
- 跑完叫我:关卡窗口新增“等跑图”,默认“跑完叫我”。AI 提交后最多等 15 秒,还没跑完就告诉你在跑,然后收工,不再一遍遍查进度(以前一张图要查 10 次左右)。跑完后,AI 工作台那一步下面有几句话,点一下复制,粘到 AI 的对话框里,它就接着看结果和图;AI 知道结果后这几句话自动消失。选“AI 自己等”就和以前一样。
- Tell AI when done: a new setting in the Gates window, on by default. The AI waits up to 15 s after queueing and, if the run still goes, says so and ends its turn instead of polling run_status. When the run ends, its step offers lines to copy into the AI's chat; they go once the AI knows. "AI waits" keeps the old way.
- 工具说明压短:日常工具集里每个工具的说明重写,去掉重复和绕的话,规则和“不要做”的都保留;AI 要读的从约 2.7 万字减到约 2.2 万字,开始任务时收到的规则从约 1900 字减到约 900 字。
- Shorter tool descriptions: the daily tools' descriptions are rewritten without repeats, keeping their rules, about 22,000 characters instead of 27,000; begin_task's rules are about 900 characters instead of 1,900.
- 修复任务画布标签关闭或切换失败后误用当前工作流的问题:操作会明确报错,重新打开或明确指定新画布后才能继续。下载缺失模型现在也跟随任务画布;留言里的 `@` 搭配和工作流按完整名称查找,不再因模糊搜索前几项占满而误报缺失。
- Task canvas actions now fail when their recorded tab is closed or cannot be activated instead of using another workflow. Missing-model downloads follow the task tab, and message references resolve exact combo/workflow names before search limits.
@@ -73,8 +75,8 @@
- **宽窗口里时间线收在中间**:右边一栏关着、窗口又很宽时,时间线、标题和输入框不再横跨整个窗口,而是收成中间约 880 像素宽的一列(像聊天软件那样),滚动条仍在最右边。时间线里的图片按自己的大小从左排开,一张图不再被撑成横跨整行的大灰框。
- **AI 每一步都知道下一步做什么**:开始任务、出图和看进度、加节点连线、问你问题得到回答,这些回复里都多了一句 `next`:比如“还在跑,等 55 秒再看”“跑完了,先放大看图再告诉你结果”“还缺这几根线,照清单连”“缺模型,先查缺哪个”。小模型不擅长自己规划,但很会照着做。
- Answers of begin_task, run_workflow and run_status, the canvas changes and an answered ask_user carry next: the one thing to do now.
- **给 AI 的工具可以选“日常”**:关卡窗口里多了 **给 AI 的工具:日常 / 完整**。日常只给 AI 19 个常用工具(照着跑、看图、换模型或 LoRA、查缺的模型、问你、留言、现状一览),AI 要读的说明从约 4.2 万字减到约 1.9 万字,Gemini Flash 这类小模型日常跑图不容易绕晕;要连线搭工作流、画面板、下载时选完整。换了以后在 AI 软件里开个新对话才生效。
- The Gates window offers AI apps either every tool or a daily set of 19 (runs of saved workflows, pictures, models, the task's own): half the reading for a small model. Calls are not limited by it; apps see a change in a new chat.
- **给 AI 的工具可以选“日常”**:关卡窗口里多了 **给 AI 的工具:日常 / 完整**。日常只给 AI 常用工具(照着跑、看图、换模型或 LoRA、查缺的模型、问你、留言、现状一览,以及自动接线搭工作流),AI 要读的说明约 2.2 万字,完整的约 4.1 万字,Gemini Flash 这类小模型日常跑图不容易绕晕;要画面板、下载时选完整。换了以后在 AI 软件里开个新对话才生效。
- The Gates window offers AI apps either every tool or a daily set (runs of saved workflows, pictures, models, the task's own, canvas building): about half the reading for a small model. Calls are not limited by it; apps see a change in a new chat.
- **照着跑:日常出图一步完成**:AI 出图时可以直接说“用哪个存好的工作流、改什么”:`run_workflow` 带上工作流名,再带上要改的提示词、反向提示词、种子、尺寸或一张图(比如你标注过的重绘图),AMB 就在任务专用标签页里打开它(第一次打开,之后接着用同一个标签页),填好这几处,其余照存好的样子不动,然后出图、等结果。尺寸只改起始的空白潜空间,高清修复里的 Latent 放大保持原来的放大尺寸。原来要 6、7 步(找工作流、打开、看画布、改提示词、改种子……),现在一步,小模型也不容易绕晕。填的内容和出图一起算一步,改画布那部分同样能 Ctrl+Z;如果你给“改画布”设的关卡比“用显卡”严,这一步按改画布的关卡来问你。画布报错是因为缺模型时,“现状一览”会让 AI 先查缺哪个模型,而不是乱换选项。
- run_workflow takes blanks for a daily run in one call: workflow (a saved one, opened in the task's tab once and reused), prompt, negative, seed, width, height and image (image_node for several LoadImage nodes); the rest stays as saved. The size goes to the latent a run starts from, never to a hires fix's latent upscale. With blanks a run asks as the stricter of the GPU's and the canvas's gate.
- **现状一览,AI 一次就知道到哪一步了**:新工具 `get_situation`,AI 调一次就拿到:任务(你最后说了什么、有几个问题在等你、定了哪些决策、出过哪些图、“AMB 之外”几次)、画布简报(每个节点一行:谁接到谁;**还缺哪几根线、各要什么类型、画布上哪些现有输出能接上**;ComfyUI 报错的节点)、最近一次出图,以及**下一步该做什么**。回复控制在约 3600 字以内,Antigravity 不会把它存成文件再让 AI 去读。新开对话或拿不准时先调它;连接说明里的那句话和规则也写上了。小模型(比如 Gemini Flash)不用再把整张图记在脑子里。
+15 -21
View File
@@ -238,7 +238,7 @@ def speak(ctx, character, text, seed=None, language="auto"):
# ---------------------------------------------------------------- registry
_NODE = {"type": "integer", "description": "A node id from describe_canvas."}
_NODE = {"type": "integer", "description": "A node id."}
_MODEL = {"type": "string", "description": "model_id from search_models."}
@@ -268,10 +268,8 @@ ACTIONS = dict([
"Puts a model into a loader node (checkpoint, LoRA, VAE…) on the canvas, replacing the one it had.",
set_model, {"node_id": _NODE, "model_id": _MODEL}, ["node_id", "model_id"], destructive=True),
_action("add_lora", "Add a LoRA",
"Inserts a LoRA loader after node after_node_id (default: the workflow's main model loader, past "
"the LoRAs already on it) and moves everything that node fed behind it. LoRAs that train the text "
"encoder also take the CLIP line (from the same node, or a UNet workflow's CLIP loader); others "
"patch the model only.",
"Inserts a LoRA loader after after_node_id (default: the main model loader, past its LoRAs) and moves "
"what that node fed behind it; the CLIP line too when the LoRA trains the text encoder.",
add_lora, {"model_id": _MODEL, "after_node_id": _NODE,
"strength_model": {"type": "number", "default": 1.0}, "strength_clip": {"type": "number", "default": 1.0}},
["model_id"]),
@@ -281,12 +279,11 @@ ACTIONS = dict([
"changes; ports to nodes outside the combo (sampler…) stay open.",
place_combo, {"name": {"type": "string", "description": "Combo name from list_combos."}}, ["name"]),
_action("check_workflow_models", "Check missing models",
"Which models the open workflow needs that this computer does not have under that name, and which "
"of them Anomalous can put back (same file found by hash, or a likely candidate).",
"Models the open workflow names that this computer lacks, and which Anomalous can put back (the same "
"file by hash, or a likely one).",
check_workflow_models, read_only=True),
_action("fix_workflow_models", "Fix missing models",
"Puts back every missing model that Anomalous found with certainty (same file by hash). "
"Likely candidates are left for the user. One Ctrl+Z step.",
"Puts back every missing model found by hash; likely ones are left to the user. One Ctrl+Z step.",
fix_workflow_models, destructive=True),
_action("download_missing_models", "Download missing models",
"Downloads the models the open workflow is missing whose source is certain: Civitai found by the "
@@ -306,24 +303,21 @@ ACTIONS = dict([
_action("download_status", "Downloads", "How the downloads of this ComfyUI run are going.", download_status,
read_only=True),
_action("run_workflow", "Run the workflow",
"Queues the open workflow in ComfyUI, like pressing Run. Uses the GPU. Follow it with run_status, which "
"says where it stands and gives its pictures when done; do not look in the output folder for them. "
"A daily run in one call: workflow (a saved one, list_workflows names them) opens it in the task's tab "
"the first time and reuses it after; prompt and negative fill its prompt boxes, seed every sampler's "
"seed, width and height its empty latent, image (a picture from search_images, or in the input folder, "
"like an inpaint_image) its LoadImage; values any other setting by name ({\"steps\": 30}; "
"{\"12\": {\"denoise\": 0.5}} when several nodes have it, as set_values); the rest stays as saved. "
"Without workflow they fill the open canvas. No describe_canvas, set_prompt or set_values needed for these.",
"Queues the workflow (uses the GPU). In the same call: workflow (a saved one) opens in the task's tab "
"once and is reused; prompt, negative, seed (every sampler), width/height (the start latent), image "
"(its LoadImage) and values (any setting by name: {\"steps\": 30}, or {\"12\": {\"denoise\": 0.5}} when "
"several nodes have it) fill it in; the rest stays. Without workflow they fill the open canvas. Then "
"run_status, never the output folder.",
run_workflow, {"batch": {"type": "integer", "minimum": 1, "maximum": 8, "default": 1},
"wait_seconds": {"type": "number", "minimum": 0, "maximum": 55, "default": 15,
"description": "Wait for execution after submission; returns actual state/results. Never resubmit while running."},
"workflow": {"type": "string", "description": "A saved workflow's name (list_workflows)."},
"description": "Seconds to wait for it to end."},
"workflow": {"type": "string", "description": "A name from list_workflows."},
"prompt": {"type": "string"}, "negative": {"type": "string"},
"seed": {"type": "integer", "minimum": 0},
"width": {"type": "integer", "minimum": 16, "maximum": 16384},
"height": {"type": "integer", "minimum": 16, "maximum": 16384},
"image": {"type": "string", "description": "subfolder/name.png in the output or input folder."},
"image_node": {"type": "integer", "description": "The LoadImage to fill when the workflow has several."},
"image": {"type": "string", "description": "subfolder/name.png, output or input folder."},
"image_node": {"type": "integer", "description": "Which LoadImage, when there are several."},
"values": {"type": "object", "description": "Other settings by name, any node's."}}),
_action("open_in_anomalous", "Show a model",
"Opens Anomalous Model Browser on this model's page, so the user sees its cover and details.",
+25 -32
View File
@@ -258,61 +258,54 @@ def open_workflow(ctx, name):
return _page("open_workflow", {"filename": filename, "name": name}, timeout=60)
_NODE_ID = {"type": "integer", "description": "A node id from describe_canvas or a tool's answer."}
_VALUES = {"type": "object", "description": "{setting name: value}; a choice takes one of its choices exactly."}
_NODE_ID = {"type": "integer", "description": "A node id."}
_VALUES = {"type": "object", "description": "{setting: value}; a choice exactly as listed."}
_NEIGHBOURS = {"oneOf": [_NODE_ID, {"type": "array", "items": _NODE_ID, "minItems": 1, "maxItems": 8}]}
CANVAS_TOOLS = dict([
_action("find_nodes", "Look up node types",
"Find built-in and custom node types. query returns up to five brief candidates, without bulky choice "
"lists. types=[chosen type] gives inputs, settings, choices, ranges and output slots when needed. "
"Use add_node with insert_after or wire_from/wire_to to add and wire in one call.",
"Node types, built in and custom. query: up to 5 brief candidates; types=[...]: their inputs, "
"choices, ranges and outputs.",
find_nodes, {"query": {"type": "string"}, "types": {"type": "array", "items": {"type": "string"}, "maxItems": MAX_FOUND}},
read_only=True),
_action("add_node", "Add a node",
"Add and wire a node in ONE call. wire_from: nodes that feed it; wire_to: nodes it feeds, where an input "
"already fed is spliced (its old source feeds the new node: a ControlNet or LoRA put in front of a KSampler). "
"insert_after=ID splices it behind that node instead; with several branches, wire_to picks them. Unique "
"types, or same-type sockets of the same name (positive to positive), wire automatically. Ambiguity changes "
"nothing and lists candidates. replace:true lets it replace an input it cannot splice. One undo step; "
"returns connections and reaches_output.",
"Adds a node, wired in the same call: wire_from feeds it, wire_to takes its output (an input already "
"fed is spliced, its old source now feeding the new node: a ControlNet or LoRA before a KSampler); "
"insert_after=ID splices it behind a node. Same type or same socket name wires itself; anything "
"ambiguous changes nothing and lists the choices; replace:true replaces what it cannot splice. One "
"Ctrl+Z step.",
add_node, {"type": {"type": "string", "description": "A node type from find_nodes, e.g. KSampler."},
"values": _VALUES, "title": {"type": "string"}, "near_node_id": _NODE_ID,
"insert_after": _NODE_ID, "insert_type": {"type": "string", "description": "Only if needed: through type, e.g. IMAGE."},
"insert_after": _NODE_ID, "insert_type": {"type": "string", "description": "Through type, only if asked (e.g. IMAGE)."},
"wire_from": _NEIGHBOURS, "wire_to": _NEIGHBOURS, "replace": {"type": "boolean"}}, ["type"]),
_action("add_nodes", "Add a group of nodes",
"Adds several nodes to the open workflow in ONE call, laid out as a block: nodes in ComfyUI's API form "
"({\"a\": {\"class_type\": \"VAEEncode\", \"inputs\": {\"pixels\": [\"#12\", 0], \"vae\": [\"#4\", \"VAE\"]}}, "
"\"b\": {\"class_type\": \"SetLatentNoiseMask\", \"inputs\": {\"samples\": [\"a\", 0], ...}, \"wire_to\": 15}}): "
"a link names a node of the group or a canvas node as \"#id\", and an output by slot or name; wire_to feeds "
"canvas nodes as add_node does (an input already fed is spliced; replace:true lets it replace one it "
"cannot splice). Everything is checked first; one Ctrl+Z step. The answer maps your keys to node ids "
"and lists what is still unwired.",
"Adds a group of nodes in one call, as one block, in ComfyUI's API form: {\"a\": {\"class_type\": "
"\"VAEEncode\", \"inputs\": {\"pixels\": [\"#12\", 0], \"vae\": [\"#4\", \"VAE\"]}}, \"b\": {..., \"inputs\": "
"{\"samples\": [\"a\", 0]}, \"wire_to\": 15}}. A link names a key of the group or a canvas node \"#id\", and "
"an output slot or name; wire_to as in add_node. Checked first; one Ctrl+Z step; the answer maps keys "
"to node ids.",
add_nodes, {"nodes": {"type": "object"}, "near_node_id": _NODE_ID, "replace": {"type": "boolean"}}, ["nodes"]),
_action("connect_nodes", "Wire two nodes",
"Wire two nodes by ID: unique matching types, and same-type sockets of the same name (positive to "
"positive), connect in one undo step. No guessing a lone CONDITIONING into positive or negative, or "
"wildcard sockets: ambiguity lists candidates, changes nothing. "
"Optional output/input select sockets explicitly; replace:true permits replacing existing wires in auto mode.",
"Wires two nodes: every unique matching type, and same-type sockets of the same name (positive to "
"positive). Anything ambiguous (a lone CONDITIONING into positive or negative, wildcards) changes "
"nothing and lists the choices; then give output and input. replace:true lets it replace wires. One "
"Ctrl+Z step.",
connect_nodes, {"from_node_id": _NODE_ID, "output": {"type": "string", "description": "Optional output slot number or name."},
"to_node_id": _NODE_ID, "input": {"type": "string"}, "replace": {"type": "boolean"}},
["from_node_id", "to_node_id"]),
_action("set_values", "Change a node's settings",
"Sets settings (steps, cfg, denoise, a sampler, a file choice, a third-party node's own…), checked "
"against what each takes. values {setting name: value} goes to the one node that has that setting; a "
"name several nodes have takes the node: {\"12\": {\"denoise\": 0.5}}. node_id sets that node's values. "
"For prompts set_prompt, for models set_model find the right ones. One Ctrl+Z step.",
"Changes settings of any node, a third party's too, checked against what each takes: {setting: value} "
"goes to the one node that has it; name the node when several do: {\"12\": {\"denoise\": 0.5}}; or "
"node_id with its settings. Prompts: set_prompt; models: set_model. One Ctrl+Z step.",
set_values, {"node_id": _NODE_ID, "values": _VALUES}, ["values"], destructive=True),
_action("remove_nodes", "Remove nodes",
"Removes nodes from the open workflow, with their wires. One Ctrl+Z step.",
remove_nodes, {"node_ids": {"type": "array", "items": _NODE_ID, "maxItems": MAX_REMOVE}}, ["node_ids"],
destructive=True),
_action("new_workflow", "Start a new workflow",
"Opens a new workflow tab in ComfyUI, leaving the user's open one as it was: blank, or built and laid "
"out from nodes, a workflow in ComfyUI's API form ({\"1\": {\"class_type\": \"CheckpointLoaderSimple\", "
"\"inputs\": {\"ckpt_name\": \"x.safetensors\"}}, \"2\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": "
"{\"text\": \"...\", \"clip\": [\"1\", 1]}}}: a value or [node id, output slot]). Everything is checked "
"before the tab opens. The answer maps your ids to the canvas's node ids.",
"Opens a new workflow tab, the user's open one untouched: blank, or built from nodes in ComfyUI's API "
"form ({\"1\": {\"class_type\": \"CLIPTextEncode\", \"inputs\": {\"text\": \"...\", \"clip\": [\"2\", 1]}}}: a value "
"or [node id, output slot]). Checked before it opens; the answer maps your ids to node ids.",
new_workflow, {"name": {"type": "string"}, "nodes": {"type": "object"}}),
_action("open_workflow", "Open a saved workflow",
"Opens one of the user's saved workflows (list_workflows names them) in a new tab, leaving the open one "
+7 -9
View File
@@ -67,22 +67,20 @@ def zoom_image(image, region=None, folder="output", compare_with="", compare_fol
return answer, extra
_REGION = {"type": "object", "description": "Fractions 0-1 of the picture, as the user's marks give theirs; default the whole picture.",
_REGION = {"type": "object", "description": "Fractions 0-1, like the user's marks; default all.",
"properties": {name: {"type": "number", "minimum": 0, "maximum": 1} for name in ("x", "y", "width", "height")}}
_FOLDER = {"type": "string", "enum": ["output", "input", "temp"], "default": "output"}
LOOK_TOOLS = dict([
_tool("zoom_image", "Look closer at a picture",
"Part of a picture shown larger, attached: to check a hand, a face, a seam or a mark's area at full detail. "
"folder: output (results), input (what LoadImage reads, e.g. inpaint_image), temp (a run's previews). "
"compare_with: a second picture, the same region attached beside it (a before and an after). Reads only. "
"Use it instead of cropping or comparing pictures with code of your own; change a picture only by running "
"a ComfyUI workflow (inpaint and the like), never by editing its file.",
zoom_image, {"image": {"type": "string", "description": "As search_images, get_image_info or run_status name it."},
"Part of a picture larger, at full detail (a hand, a face, a seam). folder: output, input or temp. "
"compare_with: a second picture, the same region beside it. Use it instead of cropping with code; "
"change pictures only by running a workflow, never by editing their files.",
zoom_image, {"image": {"type": "string", "description": "As the other tools name it."},
"region": _REGION, "folder": _FOLDER,
"compare_with": {"type": "string", "description": "A second picture to show the same region of."},
"compare_with": {"type": "string", "description": "A second picture."},
"compare_folder": {"type": "string", "enum": ["output", "input", "temp"],
"description": "compare_with's folder; default the same as folder."},
"description": "Default: folder."},
"size": {"type": "integer", "minimum": MIN_SIZE, "maximum": MAX_SIZE, "default": DEFAULT_SIZE}},
["image"]),
])
+4 -5
View File
@@ -140,13 +140,12 @@ def move_model(ctx, model_id, to_type):
MODEL_TOOLS = dict([
_action("search_models", "Search models",
"Find model files (checkpoints, LoRAs, VAEs…) by name, trigger word or base model. Returns model_id for "
"get_model, and loaded_by: the node types whose drop-down lists the file (use one of them). A file in the "
"wrong folder for what it is comes with belongs_in and placement instead: nothing loads it where it is.",
"Model files by name, trigger word or base model: model_id, and loaded_by (the node types that list "
"the file). A misfiled one comes with belongs_in: nothing loads it where it is.",
search_models, {
"query": _QUERY,
"type": {"type": "string", "description": "ComfyUI models folder, e.g. checkpoints, loras, vae, diffusion_models, model_patches."},
"base_model": {"type": "string", "description": "Part of the base model name, e.g. SDXL, Pony, Flux, SD 1.5."},
"type": {"type": "string", "description": "A models folder: checkpoints, loras, vae…"},
"base_model": {"type": "string", "description": "e.g. SDXL, Pony, Flux."},
"source": {"type": "string", "enum": ["civitai", "inferred_from_file", "not_scanned"],
"description": "Where the model's information came from."},
"limit": _LIMIT}, read_only=True),
+4 -6
View File
@@ -88,12 +88,10 @@ def get_situation(ctx, task=None):
SITUATION_TOOLS = {
"get_situation": {"run": get_situation, "context": True, "spec": {
"name": "get_situation", "title": "Where things stand",
"description": "One call, the whole picture: the task (what the user asked last, what waits for them, "
"decisions, pictures made), the canvas in brief (bounded flow chains, node lines, "
"missing inputs with matching outputs, ComfyUI's errors), the "
"latest run, and next: the one thing to do now. Call it first in a new chat on a task, and "
"whenever you are unsure, instead of describe_canvas, find_nodes or reading files.",
"inputSchema": _schema({"task": {"type": "string", "description": "Optional task id returned by begin_task."}}),
"description": "The whole picture in one call: the task (what the user asked last, what waits for them, decisions, "
"pictures), the canvas in brief (flow chains, inputs nothing feeds and what fits them, errors), the "
"latest run, and next: the one thing to do now. Call it first in a new chat, and whenever unsure.",
"inputSchema": _schema({"task": {"type": "string", "description": "begin_task's id (optional)."}}),
"annotations": {"title": "Where things stand", "readOnlyHint": True, "destructiveHint": False,
"idempotentHint": True, "openWorldHint": False},
}},
+36 -64
View File
@@ -18,26 +18,15 @@ def begin_task(ctx, title, new_task=False):
ctx.get("client") or "AI", title, new_task=bool(new_task),
session=ctx.get("session") or "", task_id=ctx.get("task"),
)
answer = {"task": task["id"], "title": task["title"], "note": "Continue this task; do not begin another for its individual steps. "
"Ask the user anything with ask_user: they answer in Anomalous's AI workbench, not in your chat. "
"Use run_workflow to submit and see real execution progress. If you submit via /prompt or another tool, "
"immediately call track_run(prompt_ids, task) with the exact IDs from YOUR submission response. "
"External work is invisible until attached; never guess ownership from the global queue. "
"When the user's setting is a tab of the task's own, Anomalous keeps your canvas work in it "
"(it copies their open workflow there before your first change); do not switch tabs yourself. "
"Every AMB tool call from now on is a step of this task in Anomalous's AI workbench, "
"where the user sees, opens and undoes them. Call report to explain a choice "
"or to hand in pictures you made another way; report with done=true at the end. "
"The user's models, pictures, node types, canvas and runs are all in AMB's tools (search_models "
"says which node loads a model, move_model fixes a misfiled one, find_nodes lists node types, "
"search_images and get_image_info read pictures, zoom_image shows a part larger or "
"compares two, run_status lists a run's previews): do not read ComfyUI's folders or "
"source, run preprocessors or call its routes yourself; each of those asks the user to "
"allow it in your app. Change pictures only by running ComfyUI workflows (inpaint and "
"the like): never edit, paint over or overwrite picture files with code of your own. "
"If you still use another tool (a command, a script, reading or writing files), "
"call log_outside right after with what you ran and why. Unsure where things stand "
"(a new chat, many changes): get_situation gives the task, the canvas in brief and the next step."}
answer = {"task": task["id"], "title": task["title"], "note": (
"Continue this task; do not begin another for its steps. Every AMB call is a step the user sees, opens and "
"undoes in Anomalous's AI workbench. Ask them with ask_user (they answer there, not in your chat); report "
"explains a choice or hands in pictures, done=true at the end. Run with run_workflow; a run queued another "
"way needs track_run(prompt_ids) with your own IDs. In the task's own tab, do not switch tabs yourself. "
"Everything is in AMB's tools (models, pictures, node types, the canvas, runs and their previews): do not "
"read ComfyUI's folders or source, run preprocessors or call its routes yourself. Change pictures only by "
"running workflows, never by editing files with code. If you use another tool anyway, log_outside right "
"after. Unsure where things stand: get_situation.")}
answer["next"] = ("get_situation shows where things stand; a daily run is one call: run_workflow(workflow: a saved "
"one, with the prompt, seed or size to change).")
box = ai_toolbox.effective(ai_tasks.get_task(task["id"]))
@@ -249,7 +238,7 @@ BLOCKS_HELP = (
"Plain text only (no markup, no links); up to 30 blocks, 12 pictures or models a block."
)
_TASK_PROP = {"type": "string", "description": "Optional task id returned by begin_task."}
_TASK_PROP = {"type": "string", "description": "begin_task's id (optional)."}
def _task_tool(name, title, description, run, properties, required=()):
@@ -271,41 +260,32 @@ TASK_TOOLS = dict([
ai_run_tracking.track_run, {"prompt_ids": {"type": "array", "items": {"type": "string"}, "minItems": 1, "maxItems": 8},
"task": _TASK_PROP, "wait_seconds": {"type": "number", "minimum": 0, "maximum": 55, "default": 15}}, ["prompt_ids"]),
_task_tool("begin_task", "Begin a task",
"Call first when you start what the user asked for: names it in Anomalous's AI workbench, where the "
"user sees each of your steps with what it made, and can open or undo it. Reuses the user's waiting "
"task or your active task, including any preliminary reads. Use new_task=true only for a separate "
"user request, never for another step of the same work.",
"Call first: names what the user asked for in Anomalous's AI workbench, where they see, open and undo "
"each of your steps. Continues the waiting or active task; new_task=true only for a separate request.",
begin_task, {"title": {"type": "string", "description": "A few words, in the user's language."},
"new_task": {"type": "boolean", "default": False}},
["title"]),
_task_tool("report", "Tell the user",
"Leaves a note in the AI workbench, where the user reads your work: why you chose something, what to "
"look at, what is left, and your answers to them. Write there what you would tell them in your chat; "
"headings (#), lists (- or 1.), **bold** and `code` are laid out. reply_to: the id of the user's message "
"or marks you answer (user_messages[].id), so your reply shows under it. images: pictures you made in "
"ComfyUI's output folder (subfolder/name.png) that Anomalous's tools did not make, so they are kept "
"with the task. done=true ends the task.",
"A note in the AI workbench, where the user reads your work: your choices, what to look at, what is "
"left, your answers. Markdown headings, lists, bold and code are laid out. reply_to: the id of the "
"user's message you answer. images: output pictures (subfolder/name.png) to keep with the task. "
"done=true ends it.",
report, {"text": {"type": "string"},
"images": {"type": "array", "items": {"type": "string"}, "maxItems": MAX_LIMIT},
"done": {"type": "boolean", "default": False},
"reply_to": {"type": "string", "description": "user_messages[].id of what you answer."},
"reply_to": {"type": "string", "description": "The message id you answer."},
"task": _TASK_PROP}),
_task_tool("log_outside", "Say what you did outside Anomalous",
"Right after you used a tool that is not Anomalous's (a shell command, a script, reading or writing "
"files, another server), say so here: what (the command or change, in a line), why (what Anomalous's "
"tools did not do for it), files (what it read, wrote or changed). The user sees it in the task's "
"timeline; Anomalous sees only the calls made to it. Prefer Anomalous's tools where they do the job.",
"Right after using a tool that is not Anomalous's (a command, a script, files, another server): what "
"you ran, why, and the files it touched, for the task's timeline. Prefer Anomalous's tools.",
log_outside, {"what": {"type": "string"}, "why": {"type": "string"},
"files": {"type": "array", "items": {"type": "string"}, "maxItems": ai_outside.MAX_FILES},
"task": _TASK_PROP},
["what"]),
_task_tool("log_decision", "Record a decision",
"Keeps a key decision of the task in Anomalous's AI workbench, where the user looks back on how the "
"task went. Talk it over wherever you like, your own chat too; once you and the user settle something "
"(a style, a model, a fix, what to drop), record it here: title (what it is about), decision (what was "
"settled), reason, rejected (the options left out), by (user: they chose; ai: your own call; "
"together), related (ids of this task's steps it is about: a run, the marks, a panel). The user's own "
"choices in Anomalous (panel answers, allowing a step, materials) are kept already; record the rest.",
"Records a settled decision of the task (a style, a model, a fix, what to drop) where the user looks "
"back on it: title, decision, reason, rejected options, by (user, ai or together), related step ids. "
"The user's own choices in Anomalous are kept already.",
log_decision, {"title": {"type": "string"}, "decision": {"type": "string"}, "reason": {"type": "string"},
"rejected": {"type": "array", "items": {"type": "string"}, "maxItems": ai_decisions.MAX_REJECTED},
"by": {"type": "string", "enum": list(ai_decisions.BY), "default": "user"},
@@ -322,12 +302,10 @@ TASK_TOOLS = dict([
"task": _TASK_PROP},
["title", "blocks"]),
_task_tool("ask_user", "Ask the user",
"Asks the user in Anomalous's AI workbench, where they watch your work, and waits for the answer: "
"a question with options to press (2-8 {label, detail, recommended}: mark the one you suggest with "
"recommended=true instead of writing it into the label) and, unless other=false, a line for their own "
"answer; no options: a question they answer in a line. Use it for every choice or preference that is "
"the user's while you work on an Anomalous task, never your own chat's question form. Returns the "
"choice (or their text); if they have not answered within wait_seconds, keep waiting with get_answers.",
"Asks the user in the AI workbench and waits: a question with 2-8 options ({label, detail, "
"recommended}; mark your suggestion recommended=true), and a line for their own answer unless "
"other=false; no options: a free answer. Use it for every choice that is the user's, not your chat's "
"question form. Unanswered within wait_seconds: wait on with get_answers.",
ask_user, {"question": {"type": "string"},
"options": {"type": "array", "maxItems": 8, "items": {"type": "object", "properties": {
"label": {"type": "string"}, "detail": {"type": "string"}, "recommended": {"type": "boolean"}}}},
@@ -346,12 +324,10 @@ TASK_TOOLS = dict([
"blocks": {"type": "array", "items": {"type": "object"}, "maxItems": ai_panels.MAX_BLOCKS},
"closed": {"type": "boolean"}, "task": _TASK_PROP}, ["panel"]),
_task_tool("run_status", "How the run is going",
"Where your task's latest workflow run stands: none (no run yet), queued (how many runs are ahead), running (which node, "
"its steps, percent, nodes done of all), done (with the pictures it saved; the first answer telling you it "
"ended, this or any other, attaches up to 4 thumbnails, so get_image_info is not needed) or failed; summary says it "
"in one sentence. wait_seconds (up to 55) waits for it to end. Runs take a while: call this instead of "
"guessing, looking in the output folder or at the screen. While a run goes, your other tools' answers "
"carry the same news as your_run, and once more when it ends.",
"Your task's latest run: none, queued (runs ahead), running (node, steps, percent), done (its "
"pictures; the first answer telling you it ended attaches up to 4) or failed; summary says it in a "
"sentence. wait_seconds (up to 55) waits for the end. Call it instead of guessing or looking in "
"folders.",
run_status, {"wait_seconds": {"type": "number", "minimum": 0, "maximum": ai_task_runs.MAX_STATUS_WAIT, "default": 0},
"task": _TASK_PROP}),
_task_tool("show_run_map", "Show what the run really was",
@@ -362,9 +338,8 @@ TASK_TOOLS = dict([
"explanation with report. title: a few words in the user's language.",
show_run_map, {"title": {"type": "string"}, "run": {"type": "string"}, "task": _TASK_PROP}, ["title"]),
_task_tool("stop_run", "Stop the run",
"Stops your task's run that is still going: its prompts still queued leave ComfyUI's queue and the one "
"running is interrupted (never anyone else's). Use it when run_status warns that the run is far too big "
"or slow and the user agrees (ask_user), or when the user asks you to stop.",
"Stops your task's run: its queued prompts leave the queue, the running one is interrupted (never "
"anyone else's). When run_status warns it is far too big and the user agrees, or when the user asks.",
stop_run, {"task": _TASK_PROP}),
_task_tool("get_toolbox", "What the user picked for you",
"The node types, whole custom node packs, combos, saved workflows, prompts and models the user picked in "
@@ -374,13 +349,10 @@ TASK_TOOLS = dict([
"begin_task says when there are any.",
get_toolbox, {"task": _TASK_PROP}),
_task_tool("get_answers", "Read the user's answers and messages",
"What the user picked or typed on your panels, and what they wrote to you in the AI workbench "
"(messages: a change of mind, 'stop', 'make it brighter'; kind=change: something they changed "
"themselves in Anomalous while you worked, keep it), since you last asked; each once. For one "
"panel (and its task's messages), or all of yours. wait_seconds (up to 55) waits when nothing is there "
"yet: for that panel's open questions, or with no panel for anything the user sends. still_waiting "
"lists the questions not answered; skipped=true is a question the user chose not to answer (go on with your "
"own judgement). Check it between steps of longer work.",
"What the user picked or typed on your panels and wrote to you in the AI workbench since you last "
"asked, each once (kind=change: something they changed themselves; keep it). panel: one panel. "
"wait_seconds (up to 55) waits when nothing is there. still_waiting: questions not answered; "
"skipped=true: they chose not to answer, go on with your own judgement.",
get_answers, {"panel": {"type": "string"},
"wait_seconds": {"type": "number", "minimum": 0, "maximum": ai_panels.MAX_WAIT, "default": 0},
"task": _TASK_PROP}),
+7 -10
View File
@@ -475,7 +475,7 @@ def _schema(properties=None, required=()):
return schema
_QUERY = {"type": "string", "description": "Words that must all appear (case-insensitive); empty lists everything."}
_QUERY = {"type": "string", "description": "Words that must all appear; empty lists all."}
_LIMIT = {"type": "integer", "minimum": 1, "maximum": MAX_LIMIT, "default": 20}
@@ -496,18 +496,15 @@ TOOLS = dict([
"Which models are matched on Civitai, which are not and why, which were never scanned, and what the last scan did.",
scan_report),
_tool("search_images", "Search output images",
"Find images ComfyUI generated, newest first, by words in their prompt, model or settings (e.g. a LoRA "
"name or 'seed:123'). Returns image paths for get_image_info. with_pictures attaches the first 6 "
"themselves (512 px), in the listed order, so you can look at them at once.",
"Output images, newest first, by words in their prompt, model or settings (a LoRA name, 'seed:123'). "
"with_pictures attaches the first 6 (512 px).",
search_images, {"query": _QUERY, "limit": _LIMIT, "page": {"type": "integer", "minimum": 1, "default": 1},
"with_pictures": {"type": "boolean", "default": False}}),
_tool("get_image_info", "Image generation settings",
"The nodes and settings that produced an output image (prompts, models, seed, sampler…) and whether the "
"user kept it as a recipe, combo or prompt. With folder=input, a picture in ComfyUI's input folder (what "
"LoadImage reads, e.g. the inpaint_image Anomalous put there), with folder=temp a run's preview (run_status "
"lists them): its size and the area LoadImage gives as MASK. include_image attaches a 512 px copy; "
"zoom_image shows a part larger. No need to look at these files any other way.",
get_image_info, {"image": {"type": "string", "description": "From search_images, e.g. 'ComfyUI_00012_.png'; with folder=input, the name LoadImage takes."},
"How an output image was made (prompts, models, seed, sampler…) and whether the user kept it. "
"folder=input: a picture LoadImage reads; folder=temp: a run's preview; both with their size and MASK "
"area. include_image attaches a 512 px copy; zoom_image shows a part larger.",
get_image_info, {"image": {"type": "string", "description": "From search_images; for folder=input, the name LoadImage takes."},
"include_image": {"type": "boolean", "default": False},
"folder": {"type": "string", "enum": ["output", "input", "temp"], "default": "output"}}, ["image"]),
_tool("list_combos", "Combos",