dzmdd's picture
Upload folder using huggingface_hub (part 2)
bc50ea0 verified
Raw History Blame Contribute Delete
17.2 kB
"""Small, lazy file store for Scene Prompt Editor revisions.
The active prompt deliberately remains in the human-readable Plan JSON. This
store keeps immutable executed revisions and one mutable draft per branch in
the run output folder without making the workflow document grow over time.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import threading
import uuid
from datetime import datetime, timezone
from typing import Any
FORMAT = "h3_scene_prompt_history_v1"
MAX_PROMPT_LENGTH = 200_000
MAX_LABEL_LENGTH = 80
_LOCK = threading.RLock()
def _safe_component(value: Any, label: str) -> str:
text = re.sub(r"[^A-Za-z0-9._-]+", "_", str(value or "").strip())
text = text.strip("._-")[:96]
if not text:
raise ValueError("A non-empty %s is required." % label)
return text
def _normalized_prompt(value: Any) -> str:
# Match Plan execution normalization so harmless leading/trailing editor
# whitespace does not create a second "executed" variant.
prompt = str(value or "").replace("\r\n", "\n").replace("\r", "\n").strip()
if len(prompt) > MAX_PROMPT_LENGTH:
raise ValueError("The scene prompt is too large.")
return prompt
def _normalized_label(value: Any) -> str:
label = " ".join(str(value or "").split())
if len(label) > MAX_LABEL_LENGTH:
raise ValueError(
"The prompt revision label must be %d characters or fewer."
% MAX_LABEL_LENGTH)
return label
def _prompt_hash(prompt: str) -> str:
return hashlib.sha256(prompt.encode("utf-8")).hexdigest()
def _timestamp() -> str:
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace(
"+00:00", "Z")
def _atomic_json(path: str, value: Any) -> None:
os.makedirs(os.path.dirname(path), exist_ok=True)
temporary = "%s.%s.tmp" % (path, uuid.uuid4().hex)
try:
with open(temporary, "w", encoding="utf-8") as handle:
json.dump(value, handle, ensure_ascii=False, indent=2)
handle.write("\n")
os.replace(temporary, path)
finally:
try:
os.unlink(temporary)
except FileNotFoundError:
pass
class PromptHistoryStore:
def __init__(self, output_root: str):
self.output_root = os.path.abspath(output_root)
def _scene_dir(self, run_name: Any, scene_id: Any) -> tuple[str, str, str]:
run = _safe_component(run_name, "H3 chain run_name")
scene = _safe_component(scene_id, "scene ID")
path = os.path.abspath(os.path.join(
self.output_root, "h3_chains", run, "prompt_history", scene))
if os.path.commonpath([self.output_root, path]) != self.output_root:
raise ValueError("Prompt history path escapes the output directory.")
return path, run, scene
@staticmethod
def _empty_index(run: str, scene: str) -> dict[str, Any]:
return {
"format": FORMAT,
"run_name": run,
"scene_id": scene,
"active_revision": None,
"revisions": [],
}
def _load_index(self, directory: str, run: str, scene: str) -> dict[str, Any]:
path = os.path.join(directory, "index.json")
if not os.path.isfile(path):
return self._empty_index(run, scene)
with open(path, "r", encoding="utf-8") as handle:
index = json.load(handle)
if (not isinstance(index, dict) or index.get("format") != FORMAT
or not isinstance(index.get("revisions"), list)):
raise ValueError("The prompt-history index is invalid.")
index["run_name"] = run
index["scene_id"] = scene
for revision in index["revisions"]:
if not isinstance(revision, dict):
raise ValueError("The prompt-history index is invalid.")
revision.setdefault("label", "")
revision.setdefault("archived_at", None)
return index
@staticmethod
def _meta(index: dict[str, Any], revision_id: Any) -> dict[str, Any] | None:
revision = str(revision_id or "")
return next((item for item in index["revisions"]
if item.get("id") == revision), None)
@staticmethod
def _revision_path(directory: str, revision_id: str) -> str:
if re.fullmatch(r"[0-9a-f]{32}", revision_id) is None:
raise ValueError("Unknown prompt revision.")
return os.path.join(directory, "%s.json" % revision_id)
def _read_revision(self, directory: str, revision_id: str) -> dict[str, Any]:
path = self._revision_path(directory, revision_id)
try:
with open(path, "r", encoding="utf-8") as handle:
value = json.load(handle)
except FileNotFoundError as exc:
raise ValueError("Prompt revision content is missing.") from exc
if not isinstance(value, dict) or value.get("id") != revision_id:
raise ValueError("Prompt revision content is invalid.")
return value
@staticmethod
def _public_index(index: dict[str, Any]) -> dict[str, Any]:
revisions = [dict(item) for item in index["revisions"]]
active_revision = index.get("active_revision")
active = next((item for item in revisions
if item.get("id") == active_revision), None)
executed = [item for item in revisions if item.get("executed_at")]
latest_executed = max(
executed,
key=lambda item: str(item.get("last_executed_at")
or item.get("executed_at") or ""),
default=None)
return {
"format": FORMAT,
"run_name": index["run_name"],
"scene_id": index["scene_id"],
"active_revision": active_revision,
"active_revision_state": (
"executed" if active and active.get("executed_at") else
"draft" if active else "empty"),
"latest_executed_revision": (
latest_executed.get("id") if latest_executed else None),
"archived_revision_count": sum(
1 for item in revisions if item.get("archived_at")),
"revisions": revisions,
}
def list(self, run_name: Any, scene_id: Any) -> dict[str, Any]:
with _LOCK:
directory, run, scene = self._scene_dir(run_name, scene_id)
return self._public_index(self._load_index(directory, run, scene))
def get(self, run_name: Any, scene_id: Any,
revision_id: Any) -> dict[str, Any]:
with _LOCK:
directory, run, scene = self._scene_dir(run_name, scene_id)
index = self._load_index(directory, run, scene)
revision = str(revision_id or "")
if self._meta(index, revision) is None:
raise ValueError("Unknown prompt revision.")
return self._read_revision(directory, revision)
def _find_prompt(self, directory: str, index: dict[str, Any],
prompt: str) -> dict[str, Any] | None:
digest = _prompt_hash(prompt)
for meta in reversed(index["revisions"]):
if meta.get("prompt_sha256") != digest:
continue
try:
revision = self._read_revision(directory, meta["id"])
except ValueError:
continue
if revision.get("prompt") == prompt:
return meta
return None
def _write_index(self, directory: str, index: dict[str, Any]) -> None:
_atomic_json(os.path.join(directory, "index.json"), index)
def _create(self, directory: str, index: dict[str, Any], prompt: str,
parent_id: str | None) -> dict[str, Any]:
now = _timestamp()
meta = {
"id": uuid.uuid4().hex,
"parent_id": parent_id,
"label": "",
"archived_at": None,
"created_at": now,
"updated_at": now,
"executed_at": None,
"last_executed_at": None,
"execution_count": 0,
"prompt_sha256": _prompt_hash(prompt),
}
revision = {"format": FORMAT, **meta, "prompt": prompt}
_atomic_json(self._revision_path(directory, meta["id"]), revision)
index["revisions"].append(meta)
index["active_revision"] = meta["id"]
self._write_index(directory, index)
return revision
def save_draft(self, run_name: Any, scene_id: Any, prompt: Any,
parent_revision: Any = None) -> dict[str, Any]:
prompt = _normalized_prompt(prompt)
with _LOCK:
directory, run, scene = self._scene_dir(run_name, scene_id)
index = self._load_index(directory, run, scene)
exact = self._find_prompt(directory, index, prompt)
if exact is not None:
if exact.get("archived_at"):
exact["archived_at"] = None
revision = self._read_revision(directory, exact["id"])
revision["archived_at"] = None
_atomic_json(
self._revision_path(directory, exact["id"]), revision)
index["active_revision"] = exact["id"]
self._write_index(directory, index)
return {
"history": self._public_index(index),
"revision": self._read_revision(directory, exact["id"]),
}
parent = str(parent_revision or index.get("active_revision") or "")
parent_meta = self._meta(index, parent)
if parent and parent_meta is None:
raise ValueError("The parent prompt revision no longer exists.")
# A draft stays mutable until it is executed. Editing an executed
# revision creates an immutable-parent child, which is the branch.
if parent_meta is not None and parent_meta.get("executed_at") is None:
now = _timestamp()
parent_meta["updated_at"] = now
parent_meta["prompt_sha256"] = _prompt_hash(prompt)
revision = self._read_revision(directory, parent)
revision.update({
"updated_at": now,
"prompt_sha256": parent_meta["prompt_sha256"],
"prompt": prompt,
})
_atomic_json(self._revision_path(directory, parent), revision)
index["active_revision"] = parent
self._write_index(directory, index)
else:
revision = self._create(
directory, index, prompt,
parent if parent_meta is not None else None)
return {
"history": self._public_index(index),
"revision": revision,
}
def activate(self, run_name: Any, scene_id: Any,
revision_id: Any) -> dict[str, Any]:
with _LOCK:
directory, run, scene = self._scene_dir(run_name, scene_id)
index = self._load_index(directory, run, scene)
revision = str(revision_id or "")
meta = self._meta(index, revision)
if meta is None:
raise ValueError("Unknown prompt revision.")
value = self._read_revision(directory, revision)
if meta.get("archived_at"):
meta["archived_at"] = None
value["archived_at"] = None
_atomic_json(self._revision_path(directory, revision), value)
index["active_revision"] = revision
self._write_index(directory, index)
return {"history": self._public_index(index), "revision": value}
def set_label(self, run_name: Any, scene_id: Any, revision_id: Any,
label: Any) -> dict[str, Any]:
label = _normalized_label(label)
with _LOCK:
directory, run, scene = self._scene_dir(run_name, scene_id)
index = self._load_index(directory, run, scene)
revision_id = str(revision_id or "")
meta = self._meta(index, revision_id)
if meta is None:
raise ValueError("Unknown prompt revision.")
revision = self._read_revision(directory, revision_id)
meta["label"] = label
revision["label"] = label
_atomic_json(
self._revision_path(directory, revision_id), revision)
self._write_index(directory, index)
return {
"history": self._public_index(index),
"revision": revision,
}
def set_archived(self, run_name: Any, scene_id: Any, revision_id: Any,
archived: Any = True) -> dict[str, Any]:
with _LOCK:
directory, run, scene = self._scene_dir(run_name, scene_id)
index = self._load_index(directory, run, scene)
revision_id = str(revision_id or "")
meta = self._meta(index, revision_id)
if meta is None:
raise ValueError("Unknown prompt revision.")
should_archive = bool(archived)
if should_archive and index.get("active_revision") == revision_id:
raise ValueError(
"The active prompt revision cannot be archived. "
"Activate another revision first.")
revision = self._read_revision(directory, revision_id)
archived_at = _timestamp() if should_archive else None
meta["archived_at"] = archived_at
revision["archived_at"] = archived_at
_atomic_json(
self._revision_path(directory, revision_id), revision)
self._write_index(directory, index)
return {
"history": self._public_index(index),
"revision": revision,
}
def delete_draft(self, run_name: Any, scene_id: Any,
revision_id: Any) -> dict[str, Any]:
with _LOCK:
directory, run, scene = self._scene_dir(run_name, scene_id)
index = self._load_index(directory, run, scene)
revision_id = str(revision_id or "")
meta = self._meta(index, revision_id)
if meta is None:
raise ValueError("Unknown prompt revision.")
if index.get("active_revision") == revision_id:
raise ValueError(
"The active prompt revision cannot be deleted. "
"Activate another revision first.")
if meta.get("executed_at"):
raise ValueError(
"Executed prompt history is protected. Archive it instead.")
if any(item.get("parent_id") == revision_id
for item in index["revisions"]):
raise ValueError(
"A prompt revision with descendants cannot be deleted. "
"Archive it instead.")
index["revisions"] = [
item for item in index["revisions"]
if item.get("id") != revision_id
]
self._write_index(directory, index)
try:
os.unlink(self._revision_path(directory, revision_id))
except FileNotFoundError:
pass
return {"history": self._public_index(index)}
def mark_executed(self, run_name: Any, scene_id: Any,
prompt: Any) -> dict[str, Any]:
prompt = _normalized_prompt(prompt)
with _LOCK:
directory, run, scene = self._scene_dir(run_name, scene_id)
index = self._load_index(directory, run, scene)
meta = self._find_prompt(directory, index, prompt)
if meta is None:
parent = str(index.get("active_revision") or "")
revision = self._create(
directory, index, prompt,
parent if self._meta(index, parent) is not None else None)
meta = self._meta(index, revision["id"])
else:
revision = self._read_revision(directory, meta["id"])
if meta.get("archived_at"):
meta["archived_at"] = None
revision["archived_at"] = None
now = _timestamp()
if meta.get("executed_at") is None:
meta["executed_at"] = now
revision["executed_at"] = now
meta["last_executed_at"] = now
meta["execution_count"] = int(meta.get("execution_count") or 0) + 1
meta["updated_at"] = now
revision.update({
"last_executed_at": now,
"execution_count": meta["execution_count"],
"updated_at": now,
})
_atomic_json(self._revision_path(directory, meta["id"]), revision)
index["active_revision"] = meta["id"]
self._write_index(directory, index)
return {"history": self._public_index(index), "revision": revision}