Files
DemonGatanjieu-Anomalous_Mo…/api/ai_tasks_routes.py
T
DemonGatanjieuandClaude Opus 5.5 c3433a9241 Pick a way for a task, and ship official example workflows
Ways: "Use it this time" in the Ways view, for one task. It joins the
task's picks as a new kind, the AI is told, and add_nodes builds that way
for its use whichever one the AI asks for. "Only" lets in the nodes a
picked way builds.

Examples: text to picture, two passes, picture to picture and repainting
an area, on ComfyUI's own nodes. One card, first on the Workflows page,
opens them in a tab of their own. The files name no model or picture; a
checkpoint and a picture that are here are filled in when one is read.
list_workflows gives them to an AI when no saved workflow fits, and they
open and run by name like saved ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:51:26 +08:00

370 lines
18 KiB
Python

"""The AI workbench's routes over ai_tasks.py (the record) and ai_panels.py (panels, answers,
messages): the list and one task (reads, open like the other pages), and the writes the page
makes - start a task, rename or remove one, answer a panel, write to the AI, answer a gate and
set the gates' policies (ai_gates.py), stop a run or say how it turned out (ai_run_control.py),
hand the AI pictures (ai_pictures.py) - (local_guard.py keeps the writes to this computer)."""
import asyncio
from aiohttp import web
from . import activity_log, ai_decisions, ai_gates, ai_marks, ai_pictures, ai_privacy, ai_run_control, ai_task_canvas, ai_task_changes, ai_task_links, ai_tasks, ai_toolbox
async def api_list(request):
try:
limit = max(1, min(300, int(request.query.get("limit", 100))))
except ValueError:
limit = 100
return web.json_response({"tasks": ai_tasks.list_tasks(limit)})
async def api_start(request):
"""POST /anomalous/ai_tasks: {title, about?, from?} - the user starts a task for the next AI app that
calls; `about` names what it is about ([{type: model | image | combo, model_id | image | name}]),
`from` the task it goes on from."""
try:
body = await request.json()
except (ValueError, UnicodeDecodeError):
body = {}
body = body if isinstance(body, dict) else {}
title = body.get("title") if isinstance(body.get("title"), str) else ""
parent = body.get("from") if isinstance(body.get("from"), str) else None
return web.json_response({"task": ai_tasks.task_summary(ai_tasks.start_task(title, about=body.get("about"), from_task=parent))})
async def api_get(request):
task = ai_tasks.get_task(request.match_info["task_id"])
if task:
task["canvas_mode"] = ai_task_canvas.mode_of(task) # the task's own choice, else the user's default
task["private"] = ai_privacy.is_private(task) # the same for privacy mode
task["decisions"], task["decisions_gap"] = ai_decisions.decisions(task), ai_decisions.gap(task)
task["links"] = ai_task_links.links(task["id"]) # the task it goes on from, those going on from it
ai_pictures.mark_gone(task) # pictures deleted since: the page says so
return web.json_response({"task": task}) if task else web.json_response({"error": "not_found"}, status=404)
async def api_change(request):
"""POST /anomalous/ai_tasks/{id}: {"title": "…"} renames it, {"remove": true} takes it off the list,
{"canvas": "new" | "current"} sets whether the AI works in a tab of the task's own, {"privacy": bool}
whether its pictures are kept from the AI (ai_privacy.py), {"from": id | null}
links it to the task it goes on from (or takes the link off)."""
task_id = request.match_info["task_id"]
try:
body = await request.json()
except (ValueError, UnicodeDecodeError):
body = None
if not isinstance(body, dict):
return web.json_response({"error": "Expected {title} or {remove: true}"}, status=400)
if body.get("remove") is True:
return web.json_response({"removed": ai_task_links.remove(task_id)})
if "canvas" in body:
try:
return web.json_response({"canvas": ai_task_canvas.set_mode(task_id, body["canvas"])})
except ValueError as error:
return web.json_response({"error": str(error)}, status=400)
if "privacy" in body:
try:
return web.json_response({"privacy": ai_privacy.set_private(task_id, body["privacy"])})
except ValueError as error:
return web.json_response({"error": str(error)}, status=400)
if "from" in body:
try:
return web.json_response({"from": ai_task_links.set_from(task_id, body["from"] if isinstance(body["from"], str) else None)})
except ai_task_links.LinkError as error:
return web.json_response({"error": str(error)}, status=400)
if "title" in body:
task = ai_tasks.rename_task(task_id, body["title"])
return web.json_response({"task": ai_tasks.task_summary(task)}) if task else web.json_response({"error": "not_found"}, status=404)
return web.json_response({"error": "Expected {title} or {remove: true}"}, status=400)
async def api_answer(request):
"""POST /anomalous/ai_tasks/{id}/answer: {step, block, choice: [ids] | text | prompts, negative, pictures | skip: true} -
the user's answer on a panel; a prompt box's texts are sealed and the AI gets tokens (ai_sealed.py)."""
from . import ai_bases, ai_panels, ai_repaint, ai_sealed
try:
body = await request.json()
except (ValueError, UnicodeDecodeError):
body = None
if not isinstance(body, dict):
return web.json_response({"error": "Expected {step, block, choice | text}"}, status=400)
task_id, step_id, skip = request.match_info["task_id"], str(body.get("step")), body.get("skip") is True
try:
if "prompts" in body and not skip:
extra = await asyncio.to_thread(ai_sealed.seal_answer, task_id, body["prompts"], body.get("negative"), body.get("pictures"))
else:
# A repaint area picked: its picture is made first (outside the lock), so the AI gets it with the answer;
# a base picked: it is kept in the output folder first (ai_bases.py).
extra = (await asyncio.to_thread(ai_repaint.prepare, task_id, step_id, body.get("choice"), body.get("text"), skip)
or await asyncio.to_thread(ai_bases.prepare, task_id, step_id, body.get("choice"), skip))
except (ai_repaint.RepaintError, ValueError, OSError) as error:
return web.json_response({"error": str(error)}, status=400)
try:
answers = ai_panels.answer(task_id, step_id, str(body.get("block")), body.get("choice"), body.get("text"), skip=skip, extra=extra)
except ai_panels.PanelError as error:
ai_repaint.discard(extra)
ai_bases.discard(extra)
return web.json_response({"error": str(error)}, status=400)
await asyncio.to_thread(ai_bases.hold, task_id, extra)
return web.json_response({"answers": answers})
async def api_typing(request):
"""POST /anomalous/ai_tasks/{id}/typing: the user is typing an answer or a message (ai_answer_wait.py)."""
from . import ai_answer_wait
if not ai_answer_wait.typing(request.match_info["task_id"]):
return web.json_response({"error": "not_found"}, status=404)
return web.json_response({"ok": True})
async def api_message(request):
"""POST /anomalous/ai_tasks/{id}/message: {text, refs?} - the user writes to the AI on that task (refs: its "@")."""
from . import ai_panels
try:
body = await request.json()
except (ValueError, UnicodeDecodeError):
body = None
if not isinstance(body, dict):
return web.json_response({"error": "Expected {text}"}, status=400)
try:
step = ai_panels.leave_message(request.match_info["task_id"], body.get("text"), body.get("refs"))
except ai_panels.PanelError as error:
return web.json_response({"error": str(error)}, status=400)
return web.json_response({"step": step})
async def _body(request):
try:
body = await request.json()
except (ValueError, UnicodeDecodeError):
return None
return body if isinstance(body, dict) else None
async def api_gate(request):
"""POST /anomalous/ai_tasks/{id}/gate: {step, decision: allow | deny, text?, remember?}."""
body = await _body(request)
if body is None:
return web.json_response({"error": "Expected {step, decision}"}, status=400)
try:
step = ai_gates.decide(request.match_info["task_id"], str(body.get("step")), body.get("decision"),
body.get("text") or "", bool(body.get("remember")))
except ValueError as error:
return web.json_response({"error": str(error)}, status=400)
return web.json_response({"step": step})
async def api_gates(request):
"""GET /anomalous/ai_gates: {policies: {gate: policy}, canvas_tab, tools}; POST {gate, policy} changes one,
POST {canvas_tab: new | current} whether new tasks work in a tab of their own (ai_task_canvas.py),
POST {privacy: bool} whether new tasks are private (ai_privacy.py),
POST {runs: call | wait}
how the AI waits for a run (ai_gates.run_wait), POST {answer_wait: 60 | 180 | 600} for the user's answer,
POST {no_answer: stop | recommended} what happens when that time runs out,
POST {nodes: any | ways} which custom nodes the AI adds itself (ai_gates.node_rule),
POST {routes: allow | ask} an AI placing nodes by itself (ai_gates.route_rule)."""
def settings():
return {"policies": ai_gates.policies(), "canvas_tab": ai_task_canvas.default_mode(), "privacy": ai_privacy.default_on(),
"runs": ai_gates.run_wait(), "answer_wait": ai_gates.answer_wait(), "no_answer": ai_gates.no_answer(),
"nodes": ai_gates.node_rule(), "routes": ai_gates.route_rule()}
if request.method == "GET":
return web.json_response(settings())
body = await _body(request)
try:
if "canvas_tab" in (body or {}):
ai_task_canvas.set_default(body["canvas_tab"])
elif "privacy" in (body or {}):
ai_privacy.set_default(body["privacy"])
elif "runs" in (body or {}):
ai_gates.set_run_wait(body["runs"])
elif "answer_wait" in (body or {}):
ai_gates.set_answer_wait(body["answer_wait"])
elif "no_answer" in (body or {}):
ai_gates.set_no_answer(body["no_answer"])
elif "nodes" in (body or {}):
ai_gates.set_node_rule(body["nodes"])
elif "routes" in (body or {}):
ai_gates.set_route_rule(body["routes"])
else:
ai_gates.set_policy((body or {}).get("gate"), (body or {}).get("policy"))
except ValueError as error:
return web.json_response({"error": str(error)}, status=400)
return web.json_response(settings())
async def api_marks(request):
"""POST /anomalous/ai_tasks/{id}/marks: {image, png, marks, text, mode, mask} - the user's marks on a picture for the AI."""
from . import ai_panels
body = await _body(request)
if body is None:
return web.json_response({"error": "Expected {image, png, marks}"}, status=400)
try:
step = await asyncio.to_thread(ai_marks.save, request.match_info["task_id"], body.get("image"),
body.get("png"), body.get("marks"), body.get("text"), None, body.get("mask"), body.get("mode"))
except ai_marks.MarksError as error:
return web.json_response({"error": str(error)}, status=400)
ai_panels.wake()
return web.json_response({"step": step})
async def api_marked(request):
"""GET /anomalous/ai_tasks/{id}/marks/{name}: a marked picture the user sent."""
path = ai_marks.path_of(request.match_info["task_id"], request.match_info["name"])
if not path:
return web.json_response({"error": "Not found"}, status=404)
return web.FileResponse(path, headers={"Cache-Control": "private, max-age=86400"})
async def api_way_pick(request):
"""POST /anomalous/ai_tasks/{id}/way: {name, on} - a way (做法) the user picks for this task, or takes back."""
from . import ai_panels
body = await _body(request) or {}
try:
step = ai_toolbox.pick_way(request.match_info["task_id"], str(body.get("name") or ""), bool(body.get("on")))
except ai_toolbox.ToolboxError as error:
return web.json_response({"error": str(error)}, status=400)
ai_panels.wake()
return web.json_response({"step": step})
async def api_toolbox_pick(request):
"""POST /anomalous/ai_tasks/{id}/toolbox: {mode, nodes, combos, workflows, prompts, models} - what the user hands the AI."""
from . import ai_panels
body = await _body(request)
try:
step = ai_toolbox.pick(request.match_info["task_id"], body if body is not None else None, (body or {}).get("mode", "prefer"))
except ai_toolbox.ToolboxError as error:
return web.json_response({"error": str(error)}, status=400)
ai_panels.wake()
return web.json_response({"step": step})
async def api_pictures(request):
"""POST /anomalous/ai_tasks/{id}/pictures: {images: ["subfolder/name.png"], text?} - the user hands the AI pictures."""
from . import ai_panels, ai_pictures
body = await _body(request)
try:
step = await asyncio.to_thread(ai_pictures.hand, request.match_info["task_id"], (body or {}).get("images"), (body or {}).get("text") or "")
except ai_pictures.PicturesError as error:
return web.json_response({"error": str(error)}, status=400)
ai_panels.wake()
return web.json_response({"step": step})
async def api_upload(request):
"""POST /anomalous/ai_pictures/upload?name= with a picture's bytes - one the user dropped or pasted
into a message, kept in the output folder (ai_pictures.save_upload): {image, filename, subfolder}."""
from . import ai_pictures
data = await request.read()
try:
image = await asyncio.to_thread(ai_pictures.save_upload, data, request.query.get("name", ""))
except ai_pictures.PicturesError as error:
return web.json_response({"error": str(error)}, status=400)
except OSError as error:
return web.json_response({"error": f"Could not keep the picture: {error}"}, status=500)
subfolder, _, filename = image.rpartition("/")
return web.json_response({"image": image, "filename": filename, "subfolder": subfolder})
async def api_stop(request):
"""POST /anomalous/ai_tasks/{id}/runs/{step}/stop - the user stops a run the AI started."""
from . import ai_panels
try:
answer = await asyncio.to_thread(ai_run_control.stop, request.match_info["step_id"], "user", request.match_info["task_id"])
except ai_run_control.RunControlError as error:
return web.json_response({"error": str(error)}, status=400)
ai_panels.wake()
return web.json_response(answer)
async def api_verdict(request):
"""POST /anomalous/ai_tasks/{id}/runs/{step}/verdict {verdict: good | bad | null} - the user's word on a run, told to the AI."""
from . import ai_panels
body = await _body(request) or {}
try:
verdict = await asyncio.to_thread(ai_run_control.set_verdict, request.match_info["task_id"], request.match_info["step_id"], body.get("verdict"))
except ai_run_control.RunControlError as error:
return web.json_response({"error": str(error)}, status=400)
ai_panels.wake()
return web.json_response({"verdict": verdict})
async def api_toolbox(request):
"""GET /anomalous/ai_toolbox?kind=all: what the picker lists; GET/POST /anomalous/ai_toolbox/favorites."""
if request.path.endswith("/favorites"):
if request.method == "GET":
return web.json_response({"favorites": ai_toolbox.favorites()})
body = await _body(request)
try:
return web.json_response({"favorites": ai_toolbox.set_favorites(body if body is not None else None)})
except ai_toolbox.ToolboxError as error:
return web.json_response({"error": str(error)}, status=400)
try:
return web.json_response(await asyncio.to_thread(ai_toolbox.catalog, request.query.get("kind", "")))
except ai_toolbox.ToolboxError as error:
return web.json_response({"error": str(error)}, status=400)
except Exception as error: # noqa: BLE001 - ComfyUI's node list or a store unreadable: say so to the page
return web.json_response({"error": str(error)}, status=502)
async def api_blocks(request):
"""GET /anomalous/ai_blocks: every ready-made block and skill as the 做法 panel shows them
(reference_blocks.status, ai_skills.status); POST {name, on} or {skill, on}: the user turns one on or off for the AI."""
from . import ai_skills, mcp_canvas_edit, reference_blocks
from .mcp_tools import ToolError
if request.method == "POST":
body = await _body(request) or {}
try:
if "skill" in body:
ai_skills.set_on(str(body.get("skill") or ""), bool(body.get("on")))
else:
reference_blocks.set_on(str(body.get("name") or ""), bool(body.get("on")))
except (reference_blocks.BlockError, ai_skills.SkillError) as error:
return web.json_response({"error": str(error)}, status=400)
try:
info = await asyncio.to_thread(mcp_canvas_edit.object_info, {"base_url": f"{request.scheme}://{request.host}"})
except ToolError as error:
return web.json_response({"error": str(error)}, status=502)
return web.json_response({"blocks": reference_blocks.status(info), "jobs": list(reference_blocks.JOBS),
"skills": ai_skills.status()})
def register_routes(app):
activity_log.add_listener(ai_task_changes.note_activity) # the user's changes while an AI works
app.router.add_get("/anomalous/ai_tasks", api_list)
app.router.add_post("/anomalous/ai_tasks", api_start)
app.router.add_get("/anomalous/ai_tasks/{task_id}", api_get)
app.router.add_post("/anomalous/ai_tasks/{task_id}", api_change)
app.router.add_post("/anomalous/ai_tasks/{task_id}/answer", api_answer)
app.router.add_post("/anomalous/ai_tasks/{task_id}/message", api_message)
app.router.add_post("/anomalous/ai_tasks/{task_id}/typing", api_typing)
app.router.add_post("/anomalous/ai_tasks/{task_id}/gate", api_gate)
app.router.add_post("/anomalous/ai_tasks/{task_id}/marks", api_marks)
app.router.add_get("/anomalous/ai_tasks/{task_id}/marks/{name}", api_marked)
app.router.add_post("/anomalous/ai_tasks/{task_id}/toolbox", api_toolbox_pick)
app.router.add_post("/anomalous/ai_tasks/{task_id}/way", api_way_pick)
app.router.add_post("/anomalous/ai_tasks/{task_id}/runs/{step_id}/stop", api_stop)
app.router.add_post("/anomalous/ai_tasks/{task_id}/runs/{step_id}/verdict", api_verdict)
app.router.add_post("/anomalous/ai_tasks/{task_id}/pictures", api_pictures)
app.router.add_post("/anomalous/ai_pictures/upload", api_upload)
app.router.add_get("/anomalous/ai_toolbox", api_toolbox)
app.router.add_get("/anomalous/ai_toolbox/favorites", api_toolbox)
app.router.add_post("/anomalous/ai_toolbox/favorites", api_toolbox)
app.router.add_get("/anomalous/ai_blocks", api_blocks)
app.router.add_post("/anomalous/ai_blocks", api_blocks)
app.router.add_get("/anomalous/ai_gates", api_gates)
app.router.add_post("/anomalous/ai_gates", api_gates)