Files
DemonGatanjieuandClaude Opus 5.5 67bf5b24c9 Add the training network and AI-run training rounds
The AI workbench's side column gets a training network view: base models
on the left, ways, skills and node packs on the right, a line for each
pair that ran, coloured by how it turned out (the user's word first, else
the AI's, dashed) and as thick as it ran often. A node or line shows its
record, last pictures and the pairs not tried yet in a card kept in sight.

"Start a training round" picks a skill (the AI's prompts with its advice
and without) or the ways of one use, up to four models and the pictures
each, and makes a task for the next AI (api/ai_training.py). Each run that
ends fills the trial it fits by what it used; the AI is told the next trial
and, once a model's trials are done, to compare them in a picks panel with
its own pick marked recommended. The record keeps the AI's pick apart from
the user's, and a picks block left to the AI takes its pick.

Tests: tests/test_ai_training.py; full suite and structure check pass.
Checked on an isolated ComfyUI: a round started from the view, four real
runs matched to their trials with the next one told each time, the
comparison panel and the user's pick reached the record and the graph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:02:59 +08:00

110 lines
6.2 KiB
Python

"""How long an AI waits for the user to answer its question (ask_user, a question on a panel).
One tool call waits at most ai_panels.MAX_WAIT (55 s, under the minute AI apps give a call), so
the answer to a question still open says whether to wait on with get_answers: until the time the
user set in the Gates window (ai_gates.answer_wait, from when it asked), and while the user is
typing an answer (the page posts POST /anomalous/ai_tasks/{id}/typing as they type) until a while
after their last key. Then the AI ends its turn and the user brings it back with a line to paste
(web/modules/ai_call_lines.js); or, when the user set so (ai_gates.no_answer "recommended"), each
open choice or picks block the AI marked a recommended option for takes it, marked as such for the user and the
AI (recommended_on_timeout), and the AI goes on; not once the user began writing (typed since the
question came): they stepped away mid-answer, and it waits for them. Typing is kept in memory only.
"""
import time
from . import ai_gates, ai_panels, ai_tasks
AFTER_TYPING = 60 # the user typed this recently: their answer is on its way
TOOK = ("The user did not answer in time and set that your recommended option goes then: Anomalous took it for "
"them (recommended_on_timeout). Go on with it; a question without a recommended option stays open for the user.")
STOP = "You waited as long as the user set: end your turn now. They answer in Anomalous and tell you in your chat to go on."
WROTE = ("The user began writing an answer, then stepped away: end your turn now. Nothing is chosen for them; they "
"finish it in Anomalous and tell you in your chat to go on.")
_typed = {} # task id -> time of the user's last key
def take_recommended(task_id, step):
"""Answers each open choice or picks block of panel step `step` that has a recommended option with it. Returns how many."""
taken = 0
for block in step["panel"].get("blocks") or []:
picked = [option["id"] for option in block.get("options") or block.get("items") or [] if option.get("recommended")]
if block.get("type") not in ("choices", "picks") or not picked or block.get("id") in (step.get("answers") or {}):
continue
try:
ai_panels.answer(task_id, step["id"], block["id"], choice=picked if block.get("multiple") else picked[:1],
extra={"recommended_on_timeout": True})
taken += 1
except ai_panels.PanelError:
pass
return taken
def typing(task_id, now=None):
"""The user is typing in task `task_id` (an answer or a message). False for no such task."""
with ai_tasks._lock:
if not ai_tasks.get_task(task_id):
return False
_typed[task_id] = time.time() if now is None else now
return True
def wait_on(panel_id, now=None, allow_recommended=True):
"""For a question of panel `panel_id` still open: {keep_waiting, seconds_left?, typing?, note}, or {}."""
with ai_tasks._lock:
task, step = ai_panels._find(panel_id)
return _wait_on(task, step, now, allow_recommended)
def _wait_on(task, step, now, allow_recommended):
"""Check typing and commit a timeout choice under the same lock as user answers."""
if not step:
return {}
now = time.time() if now is None else now
typed = _typed.get(task["id"], 0)
left = max(step["at"] + ai_gates.answer_wait(), typed + AFTER_TYPING) - now
if left <= 0:
wrote = typed >= step["at"] # a half-written answer is theirs: no option is taken over it
if allow_recommended and not wrote and ai_gates.no_answer() == "recommended" and take_recommended(task["id"], step):
return {"keep_waiting": False, "took_recommended": True, "note": TOOK}
return {"keep_waiting": False, "note": WROTE if wrote else STOP}
typing_now = now - typed < AFTER_TYPING
return {"keep_waiting": True, "seconds_left": round(left), **({"typing": True} if typing_now else {}),
"note": ("The user is typing their answer" if typing_now else "The user has not answered yet")
+ ": call get_answers(panel, wait_seconds=55) now, and again while keep_waiting is true. Do not "
"end your turn or ask the same in your own chat."}
def for_answer(answer, panel_id=None, now=None):
"""get_answers' `answer`, with how to wait on when nothing came back but questions still open:
for panel `panel_id`, or without one for the newest open question while the user's time runs."""
waiting = answer.get("still_waiting") or []
if not waiting or answer.get("answers") or answer.get("messages"):
return answer
if panel_id is None:
found = [wait_on(panel, now) for panel in dict.fromkeys(item["panel"] for item in waiting)]
live = [item for item in found if item.get("keep_waiting")]
status = max(live, key=lambda item: item["seconds_left"]) if live else next((item for item in found if item), {})
took = any(item.get("took_recommended") for item in found)
return {**answer, **status, **({"took_recommended": True} if took else {})}
return {**answer, **wait_on(panel_id, now)}
def collect(client, panel_id=None, wait_seconds=0, task_id=None):
"""Wait unlocked, then deliver a timeout choice or a racing user answer in this same call."""
answer = ai_panels.get_answers(client, panel_id, wait_seconds, task_id=task_id)
if answer.get("answers") or answer.get("messages") or not answer.get("still_waiting"):
return answer
with ai_tasks._lock:
# Input may have arrived after the long wait released its lock. Read it before choosing.
answer = ai_panels.get_answers(client, panel_id, 0, task_id=task_id)
if answer.get("answers") or answer.get("messages") or not answer.get("still_waiting"):
return answer
status = for_answer(answer, panel_id)
fresh = ai_panels.get_answers(client, panel_id, 0, task_id=task_id)
if fresh.get("answers") or fresh.get("messages"):
if any(item.get("recommended_on_timeout") for item in fresh.get("answers", [])):
fresh["note"] = TOOK
return fresh
return {**status, **fresh} if fresh.get("still_waiting") else fresh