Files
dify/api/services/dify_builder/serde.py
T
Charles YaoandClaude Opus 5 65a47018b4 feat(dify-builder): say the app's name once when the build lands
Spec N4 has the build-complete card headline read "<App name> is ready", with
the annotation 名字只在回复句里念一次;记录卡上不出现 -- said once, in that
sentence, and never repeated on the completion receipt. We said it nowhere: the
card was titled "Build plan" and the engine had no app name to say.

DifyBuilderContext now carries app_name, seeded at session creation through a
get_app_name_fn seam alongside the existing revision/authorize ones. Env.rename_app
returns the name the database actually took rather than None, so the refine
records what landed instead of what it proposed -- the service normalizes after
the engine hands off, and the two can differ.

The headline is interpolated, so it gets a strings.py template or it would
survive localization untranslated. That pattern matches almost anything ending
in "is ready", so it sits last and every more specific template gets first
refusal.

The receipt already omitted the name; there is now a test pinning that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 18:09:23 -07:00

83 lines
3.5 KiB
Python

"""Pure JSON (de)serialization for the domain ``DifyBuilderContext``.
``DifyBuilderContext`` (see ``core.dify_builder.models``) is the per-session
working state persisted, one row per version, as the ``context`` JSONB
column on ``dify_builder_session_commits``. This module has no I/O and
no SQLAlchemy dependency -- it only converts between the dataclass and a
JSON-safe ``dict``, so the SQL repository can hand the dict straight to an
``AdjustedJSON`` column and get it back unchanged. The on-disk key names are
unchanged from the pre-rename dataclass form so existing rows deserialize
as-is.
"""
from dataclasses import asdict
from typing import Any
from core.dify_builder.models import (
ChangeSet,
ChecklistError,
Diagnosis,
DifyBuilderContext,
MutationIntent,
Risk,
)
def context_to_dict(fc: DifyBuilderContext) -> dict[str, Any]:
"""Serialize a ``DifyBuilderContext`` to a JSON-safe dict.
``dataclasses.asdict`` recurses into the nested dataclasses
(``Diagnosis``, ``MutationIntent``, ``Risk``, ``ChangeSet``,
``ChecklistError``), turning each into a plain dict; every field is a
str/int/bool/list/dict/None -- so the result is JSON-safe as-is.
"""
return asdict(fc)
def context_from_dict(d: dict[str, Any]) -> DifyBuilderContext:
"""Rebuild a ``DifyBuilderContext`` from a dict produced by ``context_to_dict``.
Reconstructs the nested dataclasses explicitly (they are plain dicts
after a JSON round-trip). Every lookup is defensive (``.get`` with a
default matching the field default) so an older or partial dict still
yields a valid ``DifyBuilderContext``.
"""
diagnosis = Diagnosis(**d["diagnosis"]) if d.get("diagnosis") else None
staged_repair = [MutationIntent(**m) for m in d.get("staged_repair", [])]
risk = Risk(**d["risk"]) if d.get("risk") else None
change_set = ChangeSet(**d["change_set"]) if d.get("change_set") else None
checklist_errors = [ChecklistError(**e) for e in d.get("checklist_errors", [])]
return DifyBuilderContext(
failed_run_id=d.get("failed_run_id", ""),
diagnosis=diagnosis,
staged_repair=staged_repair,
risk=risk,
change_set=change_set,
checkpoint_id=d.get("checkpoint_id", ""),
verify_run_id=d.get("verify_run_id", ""),
test_input_ref=d.get("test_input_ref", ""),
last_snapshot_hash=d.get("last_snapshot_hash", ""),
next_seq=d.get("next_seq", 0),
source=d.get("source", ""),
checklist_errors=checklist_errors,
goal_text=d.get("goal_text", ""),
requirements=dict(d.get("requirements") or {}),
plan_items=list(d.get("plan_items") or []),
plan_version_tag=d.get("plan_version_tag", ""),
resource_selection=dict(d.get("resource_selection") or {}),
built_node_ids=list(d.get("built_node_ids") or []),
form_fields=list(d.get("form_fields") or []),
app_name=d.get("app_name", ""),
app_name_auto=bool(d.get("app_name_auto", False)),
edit_rules=dict(d.get("edit_rules") or {}),
edit_target_node_ids=list(d.get("edit_target_node_ids") or []),
paused=bool(d.get("paused", False)),
checkpoint_seq=int(d.get("checkpoint_seq", 0)),
last_structure_fingerprint=d.get("last_structure_fingerprint", ""),
recovery_class=d.get("recovery_class", ""),
skill_learning_policy=d.get("skill_learning_policy", "ask"),
model_config=dict(d.get("model_config") or {}),
reply_language=d.get("reply_language", ""),
)