themis / phase1 /scripts /research_release.py
vg15o2's picture
Moonley backend (HF Space build)
1d9bd9b
Raw
History Blame Contribute Delete
3.51 kB
"""Stable, public fingerprint for persisted research-answer compatibility."""
from __future__ import annotations
import hashlib
import inspect
import json
from pathlib import Path
from typing import Any
def _hash(value: Any) -> str:
if not isinstance(value, str):
value = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(value.encode("utf-8")).hexdigest()[:20]
def _source(*values: Any) -> str:
parts = []
for value in values:
if isinstance(value, str):
parts.append(value)
continue
if inspect.ismodule(value):
try:
path = inspect.getsourcefile(value)
parts.append(Path(path).read_text(encoding="utf-8") if path else repr(value))
except (OSError, TypeError):
parts.append(repr(value))
continue
try:
parts.append(inspect.getsource(value))
except (OSError, TypeError):
parts.append(repr(value))
return "\n\n".join(parts)
def build_research_release(corpus: Any, agent_module: Any) -> dict[str, Any]:
"""Fingerprint every input that can make a saved research answer stale.
Prompt hashes are derived from their actual text and algorithm hashes from
their function source, so ordinary prompt/retrieval edits invalidate saved
answers without a developer remembering to bump a browser-session number.
"""
coverage = corpus.coverage()
manifest = getattr(corpus, "manifest", {}) or {}
corpus_descriptor = {
"release_version": coverage.get("release_version") or manifest.get("release_version") or "legacy",
"accepted_judgments": coverage.get("accepted_judgments"),
"units": coverage.get("units"),
"paragraphs": coverage.get("paragraphs"),
"artifacts": manifest.get("artifacts") or {},
"model": manifest.get("model") or {},
}
corpus_type = type(corpus)
# Full source digests deliberately over-invalidate rather than risk serving
# an answer produced by changed ranking or verification helpers that were
# not listed individually here.
agent_source = _source(agent_module)
corpus_source = _source(corpus_type)
components = {
"corpus": _hash(corpus_descriptor),
"query_router": _hash(_source(
getattr(agent_module, "QUERY_ROUTER_VERSION", ""),
getattr(agent_module, "QUERY_BRIEF_SYS", ""),
getattr(agent_module, "query_brief", None),
)),
"retrieval": _hash(_source(
getattr(agent_module, "RETRIEVAL_VERSION", ""),
agent_source,
corpus_source,
getattr(agent_module, "structured_search_stream", None),
getattr(agent_module, "judge", None),
getattr(corpus_type, "identity_hits", None),
getattr(corpus_type, "search_lanes", None),
)),
"answer": _hash(_source(
getattr(agent_module, "ANSWER_PROMPT_VERSION", ""),
agent_source,
getattr(agent_module, "_GROUND_SYS", ""),
getattr(agent_module, "ground", None),
getattr(agent_module, "verify_claims", None),
getattr(agent_module, "_grounded_answer_text", None),
)),
}
return {
"fingerprint": _hash(components),
"components": components,
"corpus_release": corpus_descriptor["release_version"],
}
__all__ = ["build_research_release"]