- An answer past Antigravity's size is filed away unread (eight LoRA searches in the exam): search_models answers small (loaders once, four trigger words, a LoRA's author weight); describe_canvas gives a big canvas's last nodes a line each; mcp_fit.py keeps any other list answer within the size and names what it left out. - An empty canvas: run_workflow, describe_canvas and get_situation say to open a workflow first; get_image_info says a picture's workflow opens with open_workflow. - ask_user does not put a "test" question to the user; a task's picture count counts what its runs made. - AMB failed to load now and then: ComfyUI loads every file at once and ui_ai_workbench.js set ui_ai_toolbox.js's sideHost before it was made when the toolbox file ran first. Connected on first draw now; tests/load_order.mjs runs every file first. - input_facts moved to api/picture_mask.py to keep mcp_tools.py short. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
5.9 KiB
Python
105 lines
5.9 KiB
Python
"""get_situation: where an AI's task stands, in one answer small enough to stay in its chat.
|
|
|
|
The task (what the user asked last, questions and gates still waiting for them, decisions so
|
|
far, pictures made, steps done outside Anomalous), the canvas in brief (web/modules/
|
|
mcp_canvas_brief.js: flow chains, brief node lines, missing inputs and outputs that fit,
|
|
ComfyUI's errors), the latest run, and the one thing to do next. For an AI starting a new chat
|
|
on a task or unsure where it is: a small model reads this instead of describe_canvas, find_nodes
|
|
and the task's history. Unread messages of the user's come with it as with every answer
|
|
(ai_panels.attach_pending).
|
|
"""
|
|
|
|
from . import ai_decisions, ai_task_runs, ai_tasks, mcp_routes
|
|
from .mcp_bridge import PageRefused, PageUnavailable, ask_page
|
|
from .mcp_tools import _schema, tool_text
|
|
|
|
BUDGET = 3600 # characters: Antigravity files away an answer past about 4,000 and has the AI open it
|
|
EMPTY_CANVAS = ("The canvas is empty: run_workflow(workflow=<name>) opens a saved workflow and runs it in one call; open_workflow "
|
|
"opens one to change first, or a gallery picture's own by its file name; list_workflows names them and the examples.")
|
|
MAX_TEXT = 200
|
|
|
|
|
|
def _last_asked(task):
|
|
"""The user's latest words or marks in the task, in short."""
|
|
for step in reversed(task.get("steps") or []):
|
|
if step.get("kind") == "user" and step.get("tool") in ("user_message", "user_marks", "user_pictures"):
|
|
return {"what": step["tool"].removeprefix("user_"), "text": str(step.get("text") or "")[:MAX_TEXT], "id": step["id"]}
|
|
return None
|
|
|
|
|
|
def _canvas():
|
|
try:
|
|
return ask_page("describe_canvas", {"brief": True}, timeout=15)
|
|
except PageUnavailable as error:
|
|
return {"unavailable": str(error)}
|
|
except PageRefused as error: # no workflow open, or the task's tab cannot be opened
|
|
return {"unavailable": str(error)}
|
|
|
|
|
|
def _next(summary, run, canvas, record=None):
|
|
if summary["asks"]:
|
|
return f"{summary['asks']} question(s) or gate(s) wait for the user: get_answers waits for their answer."
|
|
if run.get("state") in ("queued", "running", "asking", "allowed"):
|
|
return "A run is going: run_status(wait_seconds=55) waits for it; do not run again."
|
|
if any("not in list" in str(item.get("error")) for item in canvas.get("errors") or ()):
|
|
return ("A model or file the workflow names is not on this computer: check_workflow_models says which and what "
|
|
"can stand in (fix_workflow_models, download_missing_models); do not guess other choices.")
|
|
if canvas.get("errors"):
|
|
return "ComfyUI shows errors on the canvas: fix the nodes in canvas.errors (set_values, connect_nodes, run_workflow image=), then run_workflow."
|
|
if canvas.get("unwired"):
|
|
return "Wire canvas.unwired with connect_nodes (fits lists outputs that fit each), then run_workflow."
|
|
if run.get("state") == "done" and run.get("pictures"):
|
|
return "Look at the run's pictures (zoom_image) and tell the user with report what came out."
|
|
if record and mcp_routes.waiting(record):
|
|
return mcp_routes.TOOLBOX_NOTE
|
|
if canvas.get("node_count") == 0:
|
|
return EMPTY_CANVAS
|
|
return "Go on with what the user asked (task.asked); run_workflow runs the canvas, report tells the user."
|
|
|
|
|
|
def get_situation(ctx, task=None):
|
|
client, session = ctx.get("client") or "AI", ctx.get("session") or ""
|
|
task_id = ai_tasks.active_task_id(client, session, task or ctx.get("task"))
|
|
record = ai_tasks.get_task(task_id) if task_id else None
|
|
run = {"state": "none"}
|
|
answer = {}
|
|
if record:
|
|
summary = ai_tasks.task_summary(record)
|
|
pictures = [item["filename"] for step in record.get("steps") or [] for item in step.get("results") or [] if item.get("type") == "image"]
|
|
answer["task"] = {"id": record["id"], "title": record.get("title") or "", "steps": summary["steps"],
|
|
"asked": _last_asked(record), "waiting_for_user": summary["asks"],
|
|
"decisions": ai_decisions.lines(record, limit=4), "pictures": pictures[-3:],
|
|
"outside_anomalous": summary["kinds"].get("outside", 0)}
|
|
status = ai_task_runs.status(client, session, task_id)
|
|
run = {key: status[key] for key in ("state", "summary", "pictures") if status.get(key)}
|
|
else:
|
|
summary = {"asks": 0}
|
|
answer["task"] = None
|
|
answer["task_note"] = "No task yet: begin_task names what the user asked for."
|
|
canvas = _canvas()
|
|
answer.update(run=run, canvas=canvas)
|
|
answer["next"] = _next(summary, run, canvas, record)
|
|
# Trim detail before chains and decisions; preserve actionable missing inputs and errors.
|
|
while len(tool_text(answer)) > BUDGET and canvas.get("lines"):
|
|
canvas["lines"].pop()
|
|
canvas["more_nodes"] = canvas.get("more_nodes", 0) + 1
|
|
while len(tool_text(answer)) > BUDGET and canvas.get("chains"):
|
|
canvas["chains"].pop()
|
|
canvas["more_chains"] = canvas.get("more_chains", 0) + 1
|
|
while len(tool_text(answer)) > BUDGET and record and answer["task"]["decisions"]:
|
|
answer["task"]["decisions"].pop(0)
|
|
return answer
|
|
|
|
|
|
SITUATION_TOOLS = {
|
|
"get_situation": {"run": get_situation, "context": True, "spec": {
|
|
"name": "get_situation", "title": "Where things stand",
|
|
"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},
|
|
}},
|
|
}
|