feat(dify-builder): engine hooks for language detection + item localization (M2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Charles YaoandClaude Opus 4.8 committed 2026-09-10 15:39:04 -07:00
1 parent 7e70c7e189
commit e7c7c7684a
3 files changed
+79

No files matched your search

+1
View File
@@ -243,6 +243,7 @@ class DifyBuilderContext:
# -- Build fields (Slice 2, additive) --
goal_text: str = ""
reply_language: str = "" # detected user-input language (BCP-47); drives reply/localization
requirements: dict[str, Any] = field(default_factory=dict)
plan_items: list[str] = field(default_factory=list)
plan_version_tag: str = ""
+13
View File
@@ -107,6 +107,10 @@ class Env:
# the current operation and persisted with its assistant turn, independent
# from observable execution progress.
emit_reasoning: Callable[[ReasoningEventData], None] | None = None
# Detects the user's input language (BCP-47) for a turn's user text. None -> no detection.
detect_language: Callable[[str], str] | None = None
# Localizes an about-to-be-committed item list into the given language. None -> no-op.
localize_items: Callable[[list["ConversationItem"], str], list["ConversationItem"]] | None = None
# Correlation metadata for the handler transition currently running.
# The runner resets it before every independently committed step.
session_id: str = ""
@@ -180,6 +184,8 @@ class Runner:
settled: bool,
) -> None:
at_version = session.version + 1
if self._env.localize_items is not None and context.reply_language:
items = self._env.localize_items(items, context.reply_language)
for item in items:
# Handler helpers own conversation sequence numbers; the runner is
# the only layer that knows which CAS version will make them
@@ -251,6 +257,13 @@ class Runner:
s, fc = self._env.repo.get_session(session_id)
self._env.begin_operation(s)
if self._env.detect_language is not None:
user_text = turn.action.payload.get("text") if turn.action is not None else None
if isinstance(user_text, str) and user_text.strip():
fc.reply_language = self._env.detect_language(user_text)
elif not fc.reply_language and fc.goal_text:
fc.reply_language = self._env.detect_language(fc.goal_text)
action_kind = turn.action.kind if turn.action is not None else ""
# A retried message may arrive after its user bubble committed but the
@@ -0,0 +1,65 @@
"""Engine wiring: detect language from user input; localize items before commit."""
from datetime import datetime
from core.dify_builder.models import Action, Actor, DifyBuilderContext, EntryMode, Session, Turn
from core.dify_builder.runner import Env, Runner
from core.dify_builder.state import PcState
from core.dify_builder.handlers_build import build_registry
from core.dify_builder.handlers_edit import edit_registry
from core.dify_builder.handlers_fix import fix_registry
from tests.unit_tests.core.dify_builder.fakes import FakeDifyPort, InMemoryRepository
from core.dify_builder.placeholder_agent import PlaceholderAgent
def _env(**overrides):
repo = InMemoryRepository()
env = Env(dify=FakeDifyPort(), agent=PlaceholderAgent(), repo=repo, now=lambda: datetime.min, **overrides)
return env, repo
def test_context_has_reply_language_default():
assert DifyBuilderContext().reply_language == ""
def test_localize_items_called_on_commit_with_reply_language():
seen = {}
def localize(items, language):
seen["language"] = language
for it in items:
if it.payload.get("text") == "PING":
it.payload["text"] = "PONG"
return items
env, repo = _env(localize_items=localize)
runner = Runner(env, fix_registry() | build_registry() | edit_registry())
s = Session(app_id="app", tenant_id="t", owner_account_id="a",
entry_mode=EntryMode.FIX, current_state=PcState.FIX_AWAIT_DECISION)
fc = DifyBuilderContext(reply_language="zh-Hans")
repo.create_session(s, fc, [])
# a stop action produces a settled commit with no items -> localize still invoked
runner.advance(s.id, Turn(action=Action(kind="stop", base_version=1), actor=Actor(account_id="a", tenant_id="t")))
assert seen.get("language") == "zh-Hans"
def test_detect_language_sets_reply_language_from_message_text():
detected = []
def detect(text):
detected.append(text)
return "ja"
env, repo = _env(detect_language=detect)
runner = Runner(env, fix_registry() | build_registry() | edit_registry())
s = Session(app_id="app", tenant_id="t", owner_account_id="a",
entry_mode=EntryMode.FIX, current_state=PcState.FIX_AWAIT_DECISION)
repo.create_session(s, DifyBuilderContext(), [])
runner.advance(
s.id,
Turn(action=Action(kind="message", payload={"text": "テスト", "client_turn_id": "t1"}, base_version=1),
actor=Actor(account_id="a", tenant_id="t")),
)
_, fc = repo.get_session(s.id)
assert fc.reply_language == "ja"
assert "テスト" in detected