项目文件夹

文件
Simon Willison 3bf5606b5a DAG phase 1: canonical message serialization + content hash
Adds llm/_canonical.py with canonical_message_json() and
message_content_hash(). The serialization is the hash contract for the
incoming DAG-shaped message store; snapshot tests in
tests/test_canonical.py pin the wire format.

The include_provider_metadata parameter is threaded through now so a
future semantic_hash column can be added without refactoring
(see plans/dag-provider-metadata-hashing.md).

Also commits the design docs:
- plans/dag-schema.md — full DAG storage design
- plans/dag-provider-metadata-hashing.md — Option 1 now, Option 2 later

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:12:45 -07:00

86 行
2.7 KiB
Python

"""Canonical message serialization and content hashing.
The output of ``canonical_message_json`` is a hash contract. Once shipped,
changing the canonicalization breaks dedup for every existing database.
Treat any edit to this module as a migration and update the snapshot tests
in ``tests/test_canonical.py`` deliberately.
``include_provider_metadata`` parameter exists so a future ``semantic_hash``
column (see ``plans/dag-provider-metadata-hashing.md``) can be added
without refactoring — pass ``False`` to get a hash that ignores opaque
provider tokens like Anthropic signatures and OpenAI ``encrypted_content``.
"""
import hashlib
import json
from typing import Any, Dict
from .parts import Message, Part
def _check_no_floats(value: Any, path: str = "provider_metadata") -> None:
if isinstance(value, float):
raise TypeError(
f"floats are not permitted inside {path}; use int or str instead"
)
if isinstance(value, dict):
for k, v in value.items():
_check_no_floats(v, f"{path}.{k}")
elif isinstance(value, (list, tuple)):
for i, v in enumerate(value):
_check_no_floats(v, f"{path}[{i}]")
def _canonical_part_dict(
part: Part, *, include_provider_metadata: bool
) -> Dict[str, Any]:
d = part.to_dict()
if "provider_metadata" in d:
if include_provider_metadata:
_check_no_floats(d["provider_metadata"])
else:
d.pop("provider_metadata")
return d
def canonical_message_dict(
msg: Message, *, include_provider_metadata: bool = True
) -> Dict[str, Any]:
d: Dict[str, Any] = {
"role": msg.role,
"parts": [
_canonical_part_dict(
p, include_provider_metadata=include_provider_metadata
)
for p in msg.parts
],
}
if include_provider_metadata and msg.provider_metadata:
_check_no_floats(msg.provider_metadata)
d["provider_metadata"] = msg.provider_metadata
return d
def canonical_message_json(
msg: Message, *, include_provider_metadata: bool = True
) -> bytes:
"""Return the deterministic JSON bytes for a Message.
The result feeds ``message_content_hash``. Stable under: key reordering
in dicts, unicode input (kept raw, not \\u-escaped), and empty-vs-missing
``provider_metadata``.
"""
return json.dumps(
canonical_message_dict(
msg, include_provider_metadata=include_provider_metadata
),
sort_keys=True,
ensure_ascii=False,
separators=(",", ":"),
).encode("utf-8")
def message_content_hash(msg: Message) -> str:
"""sha256 hex digest of the full canonical serialization of a Message."""
return hashlib.sha256(canonical_message_json(msg)).hexdigest()