diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ea1da..3a881c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)不用再把整张图记在脑子里。 diff --git a/api/mcp_actions.py b/api/mcp_actions.py index 861dc2f..f216735 100644 --- a/api/mcp_actions.py +++ b/api/mcp_actions.py @@ -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.", diff --git a/api/mcp_canvas_edit.py b/api/mcp_canvas_edit.py index 47e45d9..57852d4 100644 --- a/api/mcp_canvas_edit.py +++ b/api/mcp_canvas_edit.py @@ -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 " diff --git a/api/mcp_look.py b/api/mcp_look.py index 9902fbb..6d10b9b 100644 --- a/api/mcp_look.py +++ b/api/mcp_look.py @@ -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"]), ]) diff --git a/api/mcp_model_use.py b/api/mcp_model_use.py index 733fd81..dca484f 100644 --- a/api/mcp_model_use.py +++ b/api/mcp_model_use.py @@ -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), diff --git a/api/mcp_situation.py b/api/mcp_situation.py index 72a4227..ded148e 100644 --- a/api/mcp_situation.py +++ b/api/mcp_situation.py @@ -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}, }}, diff --git a/api/mcp_task_tools.py b/api/mcp_task_tools.py index 6a87c4b..fa5cce4 100644 --- a/api/mcp_task_tools.py +++ b/api/mcp_task_tools.py @@ -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}), diff --git a/api/mcp_tools.py b/api/mcp_tools.py index f510027..aafc0b1 100644 --- a/api/mcp_tools.py +++ b/api/mcp_tools.py @@ -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",