In one real task the AI built an inpaint branch with 9 add_node and 16 connect_nodes calls. add_nodes takes the whole group in ComfyUI's API form, the format models know best: - links inside the group, and to canvas nodes as "#12", with outputs by slot or name; - a node's wire_to feeds canvas nodes the way add_node does. All of it is checked before anything changes. The page lays the group out as one block in a free spot (canvas_build.placedBlock) and applies it in one transaction: one Ctrl+Z step, and a failure takes every new node out again (applyWires now takes several nodes). The answer maps the AI's keys to node ids and lists what is spliced and what is still unwired. A splice now uses only an input that nothing else in the plan feeds. A node whose latent comes from the group, or from wire_from, replaces the sampler's empty latent instead (replace:true) rather than being fed twice. It is in the daily tool set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
6.1 KiB
Python
117 lines
6.1 KiB
Python
"""What each thing an AI app can do through Anomalous's MCP endpoint is, in one place.
|
|
|
|
Every tool (mcp_tools.py reads, mcp_actions.py acts, mcp_task_tools.py the AI's own) has an
|
|
entry here: its kind (the AI workbench's colour, words and counts; None = not a step of its
|
|
own), the stage of the work it belongs to (the flow over a task: models, combo, prompt,
|
|
canvas, run, pictures, voice, library) and its gate (what it touches or costs, for the user's
|
|
rules on what needs their OK: canvas, gpu, download, network, files; None = nothing to guard).
|
|
tests/test_ai_tasks.py keeps this catalog and the tools in step.
|
|
|
|
The user's own changes in Anomalous (activity log actions) get stages too, so the ones made
|
|
while an AI works sit in the same flow as its steps (ai_tasks.note_activity).
|
|
"""
|
|
|
|
from collections import namedtuple
|
|
|
|
Entry = namedtuple("Entry", "kind stage gate")
|
|
|
|
STAGES = ("model", "combo", "prompt", "canvas", "run", "picture", "voice", "library")
|
|
GATES = ("canvas", "gpu", "download", "network", "files")
|
|
|
|
# The "daily" set the user can offer AI apps instead of all tools (ai_gates.tool_set): runs of saved
|
|
# workflows with their blanks, pictures, models, the task's own, and compact canvas building.
|
|
DAILY = frozenset({
|
|
"begin_task", "get_situation", "report", "ask_user", "get_answers", "log_decision", "log_outside",
|
|
"list_workflows", "run_workflow", "run_status", "stop_run", "search_images", "get_image_info", "zoom_image",
|
|
"search_models", "set_model", "add_lora", "check_workflow_models", "fix_workflow_models",
|
|
"find_nodes", "add_node", "add_nodes", "connect_nodes", "set_values", "new_workflow",
|
|
})
|
|
|
|
# Arguments with which a call also changes the canvas: it then asks as the stricter of its own gate and "canvas".
|
|
ALSO_CANVAS = {"run_workflow": ("workflow", "prompt", "negative", "seed", "width", "height", "image", "image_node", "values")}
|
|
|
|
TOOLS = {
|
|
# reading
|
|
"library_overview": Entry("look", "library", None),
|
|
"search_models": Entry("look", "model", None),
|
|
"get_model": Entry("look", "model", None),
|
|
"scan_report": Entry("look", "library", None),
|
|
"search_images": Entry("look", "picture", None),
|
|
"get_image_info": Entry("look", "picture", None),
|
|
"zoom_image": Entry("look", "picture", None),
|
|
"list_combos": Entry("look", "combo", None),
|
|
"list_workflows": Entry("look", "canvas", None),
|
|
"list_saved_prompts": Entry("look", "prompt", None),
|
|
"search_audio": Entry("look", "voice", None),
|
|
"recent_activity": Entry("look", "library", None),
|
|
"describe_canvas": Entry("look", "canvas", None),
|
|
"get_situation": Entry("look", None, None), # the task, the canvas in brief and the next step (mcp_situation.py)
|
|
"check_workflow_models": Entry("look", "model", None),
|
|
"download_status": Entry("look", "model", None),
|
|
"list_voices": Entry("look", "voice", None),
|
|
# acting
|
|
"set_prompt": Entry("canvas", "prompt", "canvas"),
|
|
"set_model": Entry("canvas", "model", "canvas"),
|
|
"add_lora": Entry("canvas", "model", "canvas"),
|
|
"place_combo": Entry("canvas", "combo", "canvas"),
|
|
"fix_workflow_models": Entry("canvas", "model", "canvas"),
|
|
"find_nodes": Entry("look", "canvas", None),
|
|
"add_node": Entry("canvas", "canvas", "canvas"),
|
|
"add_nodes": Entry("canvas", "canvas", "canvas"),
|
|
"connect_nodes": Entry("canvas", "canvas", "canvas"),
|
|
"set_values": Entry("canvas", "canvas", "canvas"),
|
|
"remove_nodes": Entry("canvas", "canvas", "canvas"),
|
|
"new_workflow": Entry("canvas", "canvas", "canvas"),
|
|
"open_workflow": Entry("canvas", "canvas", "canvas"),
|
|
"run_workflow": Entry("run", "run", "gpu"),
|
|
"download_missing_models": Entry("download", "model", "download"),
|
|
"download_model": Entry("download", "model", "download"),
|
|
"move_model": Entry("act", "model", "files"),
|
|
"scan_models": Entry("scan", "library", "network"),
|
|
"speak": Entry("speech", "voice", "gpu"),
|
|
"open_in_anomalous": Entry("show", "model", None),
|
|
# the AI's own: notes and panels are steps; the rest only steer the task
|
|
"report": Entry("note", None, None),
|
|
"log_decision": Entry("decision", None, None),
|
|
"log_outside": Entry("outside", None, None), # what it did with other tools (ai_outside.py, which also notes what changed)
|
|
"show_panel": Entry("panel", None, None),
|
|
"ask_user": Entry("panel", None, None),
|
|
"show_run_map": Entry("panel", None, None),
|
|
"begin_task": Entry(None, None, None),
|
|
"track_run": Entry(None, "run", None), # attaches an existing submission; creates its own run step
|
|
"update_panel": Entry(None, None, None),
|
|
"get_answers": Entry(None, None, None),
|
|
"run_status": Entry("look", "run", None),
|
|
"stop_run": Entry(None, "run", None), # ends a run step of its own; the step says who stopped it
|
|
"get_toolbox": Entry(None, None, None),
|
|
# Anomalous's card in the conversation (mcp_apps.py): its own calls, not the AI's steps
|
|
"app_task_view": Entry(None, None, None),
|
|
"app_answer": Entry(None, None, None),
|
|
}
|
|
|
|
# Activity log actions (activity_log.py ROUTE_ACTIONS, and "canvas") the user makes in Anomalous
|
|
# that belong in the task an AI is working on, with their stage. Downloads, imports, moves, scans
|
|
# and backups are left out: an AI's own calls can cause those too.
|
|
USER_ACTIONS = {
|
|
"canvas": "canvas",
|
|
"model_edit": "library", "model_cover": "library", "model_delete": "library",
|
|
"recipe_save": "canvas", "recipe_edit": "canvas", "recipe_delete": "canvas", "recipe_restore": "canvas",
|
|
"material_save": "prompt", "material_edit": "prompt", "material_delete": "prompt",
|
|
"preset_save": "run", "preset_rename": "run", "preset_delete": "run",
|
|
"image_delete": "picture", "audio_delete": "voice", "audio_save": "voice", "voice_settings": "voice",
|
|
"note_save": "library", "note_delete": "library",
|
|
}
|
|
|
|
|
|
def entry(tool, read_only=True):
|
|
"""The catalog entry of `tool`; one not listed is a read, or an act when it is not read-only."""
|
|
found = TOOLS.get(tool)
|
|
if found:
|
|
return found
|
|
return Entry("look" if read_only else "act", None, None)
|
|
|
|
|
|
def is_step(tool):
|
|
"""Whether a call of `tool` is a step of its own in a task."""
|
|
return entry(tool).kind is not None
|