Files
DumiFlex-ComfyUI-Wildcard-P…/engine/ai/pack/drafts.py
T
Dumitru PicalauandClaude dc32cb52f2 feat(ai): pack plans draft again only when needed; slot options come grouped by tag value (#118)
Plan drafts gets When needed (auto, the new default): after the first
draft, one more is asked only when score_plan finds a serious problem
(a relation that named nothing, a review fix other than request words,
a stated rule with no relation, too few prompt slots), and the better
draft is kept. Request gaps also match -ves plurals (elves → elf).

A slot's content call asks its options grouped by one axis (group_axis:
the one a rule narrows on, else one the slot offers, else the first),
at least two per value when the count allows, so every value is covered
by the first call. compile_slot flattens the groups; fill calls stay flat.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-24 21:06:34 +02:00

140 lines
5.9 KiB
Python

"""Best of N plan drafts: the planner is asked several times on the first
plan and the code keeps the draft that fits the request best.
A small model's plans vary a lot from one sample to the next: one draft
names every part the request asked for and wires the rule it gave, the
next drops the rule or a slot. Choosing is what code does well, so
``score_plan`` grades a compiled graph plan with no model call:
- relations that named nothing (``bad_refs``, after the repair call);
- the review's ``fix`` findings (what Fix all would have to change);
- request words the graph says nowhere (``request_gaps``);
- a request that states a rule ("never", "only", "matches") with no
relation in the plan;
- slots and relations the compile left out;
- a plan with fewer than two prompt slots.
``pick_draft`` returns the highest score; a tie keeps the earliest draft
(the one asked at the lowest temperature). ``extra_temperatures`` says
which drafts come after the first: "three" always asks two more, "auto"
one more only when the first has a problem worse than a missing word
(``serious``: a relation that named nothing, a review fix, a stated rule
with no relation, too few prompt slots), which keeps most of best-of-3's
gain, since a clean first draft stops at one.
"""
from __future__ import annotations
import re
from typing import Any
from engine.ai.pack.graph import request_gaps
#: Drafts the "three" setting asks for, and the temperature of each.
DRAFT_TEMPERATURES = (0.4, 0.7, 0.9)
def extra_temperatures(setting: str, plan: dict[str, Any], request: str,
library: list[dict[str, Any]] | None = None) -> tuple[float, ...]:
"""The temperatures of the drafts asked after ``plan``, the first."""
if setting == "three":
return DRAFT_TEMPERATURES[1:]
if setting == "auto" and score_plan(plan, request, library)["serious"] < 0:
return DRAFT_TEMPERATURES[1:2]
return ()
def images_seen(plan: dict[str, Any]) -> str:
"""What the first draft took from the reference images, for the
drafts asked without them (``from_images`` lines)."""
lines = [str(x) for x in plan.get("from_images") or [] if str(x).strip()]
if not lines:
return ("The reference images were read with the first draft; they aren't attached "
"again.")
return ("The reference images were read with the first draft and aren't attached again; "
"they showed: " + "; ".join(lines) + ". Plan from that.")
_RULE_CUE = re.compile(
r"\b(never|nothing but|only|always|without|except|unless|matching|match(?:es)?|"
r"goes? with|depends? on|no \w+ (?:in|with|for|on|when)|not \w+ (?:in|with|for|on))\b",
re.I)
#: Points per problem. A relation that never compiled and a review fix are
#: what the user would see first; a missing request word is softer (the
#: word may be prose).
BAD_REF = 6
REVIEW_FIX = 4
GAP = 2
NO_RULE = 8
LEFT_OUT = 1
FEW_SLOTS = 5
def _left_out(plan: dict[str, Any]) -> int:
return sum(1 for w in plan.get("warnings") or []
if isinstance(w, str) and w.startswith("Left out"))
def score_plan(plan: dict[str, Any], request: str,
library: list[dict[str, Any]] | None = None) -> dict[str, Any]:
"""``{score, serious, reasons}`` for a compiled graph plan: 0 is a plan
with no problem the code can see, each problem takes points off;
``serious`` leaves out the soft ones (request words, parts left out)."""
from engine.ai.pack.review import review_plan
graph = _got if isinstance(_got := plan.get("graph"), dict) else None
reasons: list[str] = []
score = soft = 0
bad = len(plan.get("bad_refs") or [])
if bad:
score -= BAD_REF * bad
reasons.append(f"{bad} relation(s) named nothing")
fixes = [f for f in review_plan(plan, library, request=request) if f["level"] == "fix"]
if fixes:
score -= REVIEW_FIX * len(fixes)
# The review files request words too: soft, as below.
soft += REVIEW_FIX * sum(1 for f in fixes if f["type"] == "request_gap")
reasons.append(f"{len(fixes)} review fix(es)")
if graph is not None:
gaps = request_gaps(graph, request)
if gaps:
score -= GAP * len(gaps)
soft += GAP * len(gaps)
reasons.append("request words left out: " + ", ".join(gaps))
if _RULE_CUE.search(request or "") and not graph["relations"]:
score -= NO_RULE
reasons.append("the request states a rule and the plan has no relation")
prompt = [s for s in graph["slots"] if s["show"] == "prompt"]
if len(prompt) < 2:
score -= FEW_SLOTS
reasons.append(f"{len(prompt)} prompt slot(s)")
left = _left_out(plan)
if left:
score -= LEFT_OUT * left
soft += LEFT_OUT * left
reasons.append(f"{left} part(s) left out")
return {"score": score, "serious": score + soft, "reasons": reasons}
def pick_draft(drafts: list[dict[str, Any]], request: str,
library: list[dict[str, Any]] | None = None) -> tuple[int, list[dict[str, Any]]]:
"""``(index, scores)``: the best of ``drafts`` (compiled plans) and
every draft's ``{score, reasons, title}``. Raises ValueError on an
empty list."""
if not drafts:
raise ValueError("no draft to pick from")
scores = [{**score_plan(p, request, library), "title": p.get("title") or ""}
for p in drafts]
best = max(range(len(drafts)), key=lambda i: (scores[i]["score"], -i))
return best, scores
def draft_line(best: int, scores: list[dict[str, Any]]) -> str:
"""What ``plan["applied"]`` says about the pick."""
if len(scores) < 2:
return ""
s = scores[best]
why = "; ".join(s["reasons"]) or "no problem found"
others = ", ".join(str(x["score"]) for i, x in enumerate(scores) if i != best)
return (f"Kept draft {best + 1} of {len(scores)} (score {s['score']}: {why}; "
f"the others scored {others})")