Files
EricRollei-Local_LLM_Prompt…/surreal_support.py
T
Eric HissandCopilot 373ea8bda3 v2.2.0: Prompt Builder nodes, wildcard review tool, docs refresh
- Add Wildcard Prompt Composer and Wildcard Character Composer builder nodes (core/builder, web/prompt_builder.js, builders/, vp wildcard lists)

- Add browser-based wildcard review tool (tools/wildcard_review) with manifest-based undo

- Add wildcard catalog/curation, surreal compiler/lint/scene, profile token budget

- README: document all 18 nodes, bump to v2.2.0, fix stale node/platform lists; CHANGELOG 2.2.0

- Rewrite NODE_COMPARISON for 18 nodes; fix QUICKSTART (dead test_setup.py ref, menu path) and QUICK_REFERENCE node list

- gitignore review manifests and CLAUDE.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-28 20:36:44 -07:00

137 lines
6.6 KiB
Python

"""
Surreal engine support shared by the nodes: builds the engine on the package's wildcard library (so the user's
packs and their iconography file take part), connects the prompt database as the personal archive, and offers
the one call the existing presets use.
Author: Eric Hiss (GitHub: EricRollei)
License: Dual License (Non-Commercial and Commercial Use). See LICENSE.
"""
import os
import random
import re
from typing import Any, Callable, List, Optional, Tuple
from .core.strategies.surreal import SurrealEngine, SurrealResult, SurrealSettings, SurrealSources, parse_critic
from .core.strategies.surreal.scene import parse_scene_inventory, scene_messages
from .wildcard_support import get_wildcard_engine
ARCHIVE_POOL = 200
ARCHIVE_WORDS = 28
def _archive(seed: int) -> Optional[Callable[[str, int], List[str]]]:
"""Fragments of the user's own saved prompts, or None when there is no database yet (never creates one)."""
try:
from .prompt_database import DB_PATH, PromptDatabase
if not os.path.isfile(DB_PATH):
return None
rows, total = PromptDatabase().search(query="", limit=ARCHIVE_POOL, order_by="oldest")
texts = [str(row.get("prompt_text") or "").strip() for row in rows]
texts = [text for text in texts if len(text.split()) >= 6]
if not texts:
return None
except Exception as error:
print(f"[Surreal] prompt database not used as archive: {error}")
return None
rng = random.Random(seed)
def fragments(idea: str, count: int) -> List[str]:
picked = rng.sample(texts, min(count, len(texts)))
return [" ".join(re.sub(r"\s+", " ", text).split()[:ARCHIVE_WORDS]).rstrip(",;:") for text in picked]
return fragments
def get_surreal_engine(seed: int = 0, use_archive: bool = True) -> SurrealEngine:
library = get_wildcard_engine().library
return SurrealEngine(SurrealSources(lookup=library.lookup, archive=_archive(seed) if use_archive else None))
def idea_distance(idea: str, embedder_choice: str, clip: Any = None) -> Tuple[Optional[Callable[[str], float]], List[str]]:
"""(distance function or None, notes). distance(text) is 0..1: how far a phrase is from the idea in meaning."""
if (embedder_choice or "none") == "none" or not (idea or "").strip():
return None, []
try:
import numpy as np
from .core.variability import VectorCache
from .variability_support import CACHE_DIR, get_embedder
embedder, notes = get_embedder(embedder_choice, clip)
if embedder.name.startswith("lexical"):
return None, notes + ["no semantic embedder available, so distance from the idea is not scored"]
cache = VectorCache(embedder, CACHE_DIR)
idea_vector = cache.vectors([idea])[0]
def distance(text: str) -> float:
vector = cache.vectors([text])[0]
return float(min(1.0, max(0.0, 1.0 - float(np.dot(idea_vector, vector)))))
return distance, notes + [f"distance from the idea scored with {embedder.describe()}"]
except Exception as error:
return None, [f"embedder unavailable ({error}); distance from the idea is not scored"]
def surreal_brief(idea: str, settings: SurrealSettings, seed: Optional[int] = None, *, llm: Any = None, max_tokens: int = 2200,
embedder: str = "none", clip: Any = None, use_archive: bool = True) -> SurrealResult:
"""
One idea -> ordinary-scene inventory -> premise -> structured render plan -> instruction block. With `llm`
(an object with send_prompt), the first call inventories the unchanged scene and the second is the critic.
Either stage can fail independently; the code critic remains the fallback. Never raises.
"""
try:
if seed is None:
seed = random.SystemRandom().randrange(0, 0x7FFFFFFF)
engine = get_surreal_engine(seed, use_archive)
distance, notes = idea_distance(idea, embedder, clip)
inventory = None
inventory_problems: List[str] = []
if llm is not None:
system, user = scene_messages(idea)
try:
response = llm.send_prompt(system, user, max_tokens=min(max_tokens, 1400))
if response.get("success"):
inventory, inventory_problems = parse_scene_inventory(response.get("response", ""))
else:
inventory_problems = [str(response.get("error") or "the scene inventory LLM call failed")]
except Exception as error:
inventory_problems = [f"scene inventory LLM backend error: {error}"]
plan = engine.plan(idea, settings, seed, distance=distance, inventory=inventory)
plan.notes.extend(notes)
if inventory_problems:
plan.notes.append("ordinary scene inventory not used: " + "; ".join(inventory_problems))
verdict, problems = None, None
if llm is not None and plan.candidates:
system, user = engine.critic_messages(plan)
try:
response = llm.send_prompt(system, user, max_tokens=max_tokens)
if response.get("success"):
verdict, problems = parse_critic(response.get("response", ""), plan.candidates)
else:
problems = [str(response.get("error") or "the LLM call failed")]
except Exception as error:
problems = [f"LLM backend error: {error}"]
return engine.finish(plan, verdict, problems)
except Exception as error: # a strategy must never break a workflow
import traceback
traceback.print_exc()
return SurrealResult(text=idea or "", instructions="", why=f"Surreal engine error: {error}. The idea was passed through unchanged.", ok=False)
def preset_plan(idea: str, seed: Optional[int], medium: str) -> Tuple[str, str, str]:
"""For legacy preset surfaces: (compiled transformed idea, instruction block, one-line note)."""
result = surreal_brief(idea, SurrealSettings(medium=medium), seed, llm=None, use_archive=True)
if not result.ok or not result.instructions:
return idea, "", "surreal engine produced nothing; the preset's style words were not replaced"
operation = ", ".join(result.operations)
return result.text, result.instructions, f"surreal engine: {operation}"
def preset_block(idea: str, seed: Optional[int], medium: str) -> Tuple[str, str]:
"""
For the existing `surreal` preset / genre values of the older nodes: (instruction block, one-line note).
Code critic only - those nodes make one LLM call and that call carries out the premise. ('', note) on failure.
"""
_text, block, note = preset_plan(idea, seed, medium)
return block, note