Files
DemonGatanjieu-Anomalous_Mo…/api/action_catalog.py
T
DemonGatanjieuandClaude Opus 5.5 00958578fb AI workbench: one action catalog; your own changes join the AI's task; tick two pictures to compare
- api/action_catalog.py: every MCP tool's kind, stage of the work and gate in one place (a test
  keeps it and the tools in step); steps now carry their stage.
- api/ai_task_changes.py: changes the user makes in Anomalous while an AI works become steps of
  its task, reach the AI once with its next call (kind "change": keep it), and the activity
  entry names the task; the activity page links to it ("AI task ->").
- api/ai_task_runs.py: run watching moved out of ai_tasks.py (600-line rule); a step a restart
  left "running" is marked stopped when the record is read again.
- Pictures: inspector / compare with canvas / keep as tools on hover instead of three buttons
  under every picture; tick any two (any step or task) and compare from the bar at the foot of
  the page; the results strip shows thumbnails again.

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

83 lines
3.9 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; None = nothing to guard).
tests/test_action_catalog.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")
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),
"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),
"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"),
"run_workflow": Entry("run", "run", "gpu"),
"download_missing_models": Entry("download", "model", "download"),
"download_model": Entry("download", "model", "download"),
"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),
"show_panel": Entry("panel", None, None),
"begin_task": Entry(None, None, None),
"update_panel": Entry(None, None, None),
"get_answers": 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