mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
feat(dify-builder): real governance tail — policy branch + build.await_learning (C-2)
Rework handle_governance_feedback into the 3-policy branch (automatic/ disabled/ask) reading fc.skill_learning_policy, and add handle_await_learning to resolve the ask-policy accept_learning/skip_learning turn. Both funnel into a shared _emit_completion helper for the build-complete summary. Registered BUILD_AWAIT_LEARNING in build_registry(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
893ffedbf0
commit
bad0046ccf
2 files changed
+155
-21
No files matched your search
@@ -12,6 +12,7 @@ import uuid
|
||||
|
||||
from core.dify_builder.contract import (
|
||||
AssistantTurnItem,
|
||||
BuildLearningCard,
|
||||
ChallengeCard,
|
||||
ChangeSetCard,
|
||||
CheckpointCard,
|
||||
@@ -20,6 +21,7 @@ from core.dify_builder.contract import (
|
||||
ErrorCard,
|
||||
FormCard,
|
||||
FormField,
|
||||
NoticeItem,
|
||||
PlanCard,
|
||||
PublishCard,
|
||||
ResourceSelectCard,
|
||||
@@ -31,12 +33,21 @@ from core.dify_builder.contract import (
|
||||
TraceStep,
|
||||
)
|
||||
from core.dify_builder.handlers_fix import action_string, append_card, perform_revert
|
||||
from core.dify_builder.models import ChangeSet, Checkpoint, DifyBuilderContext, Session, Snapshot, Turn
|
||||
from core.dify_builder.models import (
|
||||
ChangeSet,
|
||||
Checkpoint,
|
||||
ConversationItem,
|
||||
DifyBuilderContext,
|
||||
Session,
|
||||
Snapshot,
|
||||
Turn,
|
||||
)
|
||||
from core.dify_builder.runner import Env, Handler, StepResult
|
||||
from core.dify_builder.state import PcState
|
||||
|
||||
__all__ = [
|
||||
"build_registry",
|
||||
"handle_await_learning",
|
||||
"handle_capability_check",
|
||||
"handle_execution",
|
||||
"handle_goal_analysis",
|
||||
@@ -58,6 +69,16 @@ def _emit_canvas(env: Env, event: str, **extra) -> None:
|
||||
env.emit_canvas({"event": event, **extra})
|
||||
|
||||
|
||||
def _emit_completion(fc: DifyBuilderContext) -> list[ConversationItem]:
|
||||
"""Shared build-complete summary, emitted on every governance-tail exit."""
|
||||
rows = [
|
||||
SummaryRow(label="Workflow", value="Start -> Knowledge -> LLM -> End"),
|
||||
SummaryRow(label="Nodes", value=str(len(fc.built_node_ids))),
|
||||
SummaryRow(label="Status", value="Complete"),
|
||||
]
|
||||
return append_card(fc, SummaryCard(variant="completion", title="Build complete", rows=rows))
|
||||
|
||||
|
||||
_REQUIREMENT_FIELDS = [
|
||||
FormField(key="report_types", label="Report types", type="text"),
|
||||
FormField(key="audience", label="Audience", type="text"),
|
||||
@@ -481,16 +502,42 @@ def handle_publish(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) ->
|
||||
|
||||
|
||||
def handle_governance_feedback(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
|
||||
"""(working, auto) Governance tail is spec-deferred (no body). As the
|
||||
handler transitioning INTO the terminal build.complete, it emits the
|
||||
completion summary. Reached both via publish and via keep_draft (which
|
||||
skips publish), so the status row is scenario-neutral."""
|
||||
rows = [
|
||||
SummaryRow(label="Workflow", value="Start -> Knowledge -> LLM -> End"),
|
||||
SummaryRow(label="Nodes", value=str(len(fc.built_node_ids))),
|
||||
SummaryRow(label="Status", value="Complete"),
|
||||
]
|
||||
items = append_card(fc, SummaryCard(variant="completion", title="Build complete", rows=rows))
|
||||
"""(working, auto) Governance tail: apply the skill-learning policy.
|
||||
automatic -> learn + accepted card; disabled -> skipped card; ask ->
|
||||
emit the pending prompt and rest at build.await_learning. Reached via both
|
||||
publish and keep_draft (scenario-neutral)."""
|
||||
policy = fc.skill_learning_policy or "ask"
|
||||
if policy == "automatic":
|
||||
descriptor = env.agent.learn_from_build(
|
||||
fc.goal_text, dict(fc.requirements), list(fc.plan_items), list(fc.built_node_ids)
|
||||
)
|
||||
items = append_card(fc, BuildLearningCard(policy="automatic", state="accepted"))
|
||||
items += append_card(fc, NoticeItem(text=descriptor))
|
||||
items += _emit_completion(fc)
|
||||
return StepResult(next=PcState.BUILD_COMPLETE, context=fc, items=items)
|
||||
if policy == "disabled":
|
||||
items = append_card(fc, BuildLearningCard(policy="disabled", state="skipped"))
|
||||
items += _emit_completion(fc)
|
||||
return StepResult(next=PcState.BUILD_COMPLETE, context=fc, items=items)
|
||||
# ask (default): prompt, then rest for the user's accept/skip.
|
||||
items = append_card(fc, BuildLearningCard(policy="ask", state="pending"))
|
||||
return StepResult(next=PcState.BUILD_AWAIT_LEARNING, context=fc, items=items)
|
||||
|
||||
|
||||
def handle_await_learning(env: Env, turn: Turn, s: Session, fc: DifyBuilderContext) -> StepResult:
|
||||
"""(waiting) Resolve the ask-policy skill-learning prompt. accept_learning
|
||||
-> learn + accepted decision; anything else (skip_learning / absent) ->
|
||||
skipped. Either way emit the completion summary and reach build.complete."""
|
||||
kind = turn.action.kind if turn.action is not None else ""
|
||||
if kind == "accept_learning":
|
||||
descriptor = env.agent.learn_from_build(
|
||||
fc.goal_text, dict(fc.requirements), list(fc.plan_items), list(fc.built_node_ids)
|
||||
)
|
||||
items = append_card(fc, DecisionItem(text="Accepted skill learning"))
|
||||
items += append_card(fc, NoticeItem(text=descriptor))
|
||||
else:
|
||||
items = append_card(fc, DecisionItem(text="Skipped skill learning"))
|
||||
items += _emit_completion(fc)
|
||||
return StepResult(next=PcState.BUILD_COMPLETE, context=fc, items=items)
|
||||
|
||||
|
||||
@@ -530,5 +577,6 @@ def build_registry() -> dict[PcState, Handler]:
|
||||
PcState.BUILD_REVIEW: handle_review,
|
||||
PcState.BUILD_PUBLISH: handle_publish,
|
||||
PcState.BUILD_GOVERNANCE_FEEDBACK: handle_governance_feedback,
|
||||
PcState.BUILD_AWAIT_LEARNING: handle_await_learning,
|
||||
PcState.BUILD_REVERTED: handle_reverted,
|
||||
}
|
||||
@@ -21,11 +21,11 @@ def _actor() -> Actor:
|
||||
return Actor(account_id="acc-1", tenant_id="tenant-1")
|
||||
|
||||
|
||||
def _new_env(dify=None, emit_canvas=None) -> tuple[Env, InMemoryRepository]:
|
||||
def _new_env(dify=None, emit_canvas=None, agent=None) -> tuple[Env, InMemoryRepository]:
|
||||
repo = InMemoryRepository()
|
||||
env = Env(
|
||||
dify=dify or FakeDifyPort(),
|
||||
agent=PlaceholderAgent(),
|
||||
agent=agent or PlaceholderAgent(),
|
||||
repo=repo,
|
||||
now=lambda: datetime.min,
|
||||
emit_canvas=emit_canvas,
|
||||
@@ -329,20 +329,97 @@ def test_publish_calls_dify_and_advances_to_governance_feedback():
|
||||
assert {"event": "publish_workflow"} in events
|
||||
|
||||
|
||||
def test_governance_feedback_emits_completion_summary_and_reaches_complete():
|
||||
def test_governance_automatic_learns_and_reaches_complete():
|
||||
from core.dify_builder.handlers_build import handle_governance_feedback
|
||||
from tests.unit_tests.core.dify_builder.fakes import FakeBuildDifyPort, StubAgent
|
||||
|
||||
env, repo = _new_env()
|
||||
env, repo = _new_env(dify=FakeBuildDifyPort(), agent=StubAgent())
|
||||
s = _seed_build_session(
|
||||
repo,
|
||||
PcState.BUILD_GOVERNANCE_FEEDBACK,
|
||||
built_node_ids=["start", "knowledge_retrieval", "llm", "end"],
|
||||
skill_learning_policy="automatic",
|
||||
built_node_ids=["a", "b"],
|
||||
)
|
||||
res = handle_governance_feedback(env, Turn(actor=_actor()), *repo.get_session(s.id))
|
||||
assert res.next == PcState.BUILD_COMPLETE
|
||||
summary = next(i for i in res.items if i.kind == "summary")
|
||||
assert summary.payload["variant"] == "completion"
|
||||
assert summary.payload["rows"]
|
||||
kinds = [i.kind for i in res.items]
|
||||
assert "build_learning" in kinds and "summary" in kinds
|
||||
assert env.agent.learn_calls == 1 # seam called for automatic
|
||||
|
||||
|
||||
def test_governance_disabled_skips_and_reaches_complete():
|
||||
from core.dify_builder.handlers_build import handle_governance_feedback
|
||||
from tests.unit_tests.core.dify_builder.fakes import FakeBuildDifyPort, StubAgent
|
||||
|
||||
env, repo = _new_env(dify=FakeBuildDifyPort(), agent=StubAgent())
|
||||
s = _seed_build_session(
|
||||
repo,
|
||||
PcState.BUILD_GOVERNANCE_FEEDBACK,
|
||||
skill_learning_policy="disabled",
|
||||
built_node_ids=["a"],
|
||||
)
|
||||
res = handle_governance_feedback(env, Turn(actor=_actor()), *repo.get_session(s.id))
|
||||
assert res.next == PcState.BUILD_COMPLETE
|
||||
# build_learning present with state skipped; seam NOT called
|
||||
bl = [i for i in res.items if i.kind == "build_learning"][0]
|
||||
assert bl.payload["state"] == "skipped"
|
||||
assert getattr(env.agent, "learn_calls", 0) == 0
|
||||
|
||||
|
||||
def test_governance_ask_rests_at_await_learning_with_pending_card():
|
||||
from core.dify_builder.handlers_build import handle_governance_feedback
|
||||
from tests.unit_tests.core.dify_builder.fakes import FakeBuildDifyPort, StubAgent
|
||||
|
||||
env, repo = _new_env(dify=FakeBuildDifyPort(), agent=StubAgent())
|
||||
s = _seed_build_session(
|
||||
repo,
|
||||
PcState.BUILD_GOVERNANCE_FEEDBACK,
|
||||
skill_learning_policy="ask",
|
||||
built_node_ids=["a"],
|
||||
)
|
||||
res = handle_governance_feedback(env, Turn(actor=_actor()), *repo.get_session(s.id))
|
||||
assert res.next == PcState.BUILD_AWAIT_LEARNING
|
||||
bl = [i for i in res.items if i.kind == "build_learning"][0]
|
||||
assert bl.payload["policy"] == "ask" and bl.payload["state"] == "pending"
|
||||
assert getattr(env.agent, "learn_calls", 0) == 0 # not learned until accepted
|
||||
|
||||
|
||||
def test_await_learning_accept_learns_and_completes():
|
||||
from core.dify_builder.handlers_build import handle_await_learning
|
||||
from tests.unit_tests.core.dify_builder.fakes import FakeBuildDifyPort, StubAgent
|
||||
|
||||
env, repo = _new_env(dify=FakeBuildDifyPort(), agent=StubAgent())
|
||||
s = _seed_build_session(
|
||||
repo,
|
||||
PcState.BUILD_AWAIT_LEARNING,
|
||||
skill_learning_policy="ask",
|
||||
built_node_ids=["a"],
|
||||
)
|
||||
res = handle_await_learning(
|
||||
env, Turn(action=Action(kind="accept_learning", base_version=1), actor=_actor()), *repo.get_session(s.id)
|
||||
)
|
||||
assert res.next == PcState.BUILD_COMPLETE
|
||||
kinds = [i.kind for i in res.items]
|
||||
assert "decision" in kinds and "summary" in kinds
|
||||
assert env.agent.learn_calls == 1
|
||||
|
||||
|
||||
def test_await_learning_skip_completes_without_learning():
|
||||
from core.dify_builder.handlers_build import handle_await_learning
|
||||
from tests.unit_tests.core.dify_builder.fakes import FakeBuildDifyPort, StubAgent
|
||||
|
||||
env, repo = _new_env(dify=FakeBuildDifyPort(), agent=StubAgent())
|
||||
s = _seed_build_session(
|
||||
repo,
|
||||
PcState.BUILD_AWAIT_LEARNING,
|
||||
skill_learning_policy="ask",
|
||||
built_node_ids=["a"],
|
||||
)
|
||||
res = handle_await_learning(
|
||||
env, Turn(action=Action(kind="skip_learning", base_version=1), actor=_actor()), *repo.get_session(s.id)
|
||||
)
|
||||
assert res.next == PcState.BUILD_COMPLETE
|
||||
assert getattr(env.agent, "learn_calls", 0) == 0
|
||||
|
||||
|
||||
def test_reverted_retry_returns_to_initial_plan():
|
||||
@@ -372,6 +449,7 @@ def test_build_registry_covers_all_non_terminal_build_states():
|
||||
PcState.BUILD_REVIEW,
|
||||
PcState.BUILD_PUBLISH,
|
||||
PcState.BUILD_GOVERNANCE_FEEDBACK,
|
||||
PcState.BUILD_AWAIT_LEARNING,
|
||||
PcState.BUILD_REVERTED,
|
||||
}
|
||||
assert PcState.BUILD_COMPLETE not in build_registry() # terminal: no handler
|
||||
@@ -418,12 +496,17 @@ def test_full_build_flow_goal_to_complete():
|
||||
out = runner.advance(s.id, Turn(action=Action(kind="run_test", base_version=out.version), actor=_actor()))
|
||||
assert out.current_state == PcState.BUILD_REVIEW
|
||||
|
||||
# 7) publish_workflow -> build.publish (auto) -> governance_feedback (auto) -> build.complete
|
||||
# 7) publish_workflow -> build.publish (auto) -> governance_feedback (auto)
|
||||
# -> rests at build.await_learning (default policy "ask")
|
||||
publish_action = Action(kind="publish_workflow", base_version=out.version)
|
||||
out = runner.advance(s.id, Turn(action=publish_action, actor=_actor()))
|
||||
assert out.current_state == PcState.BUILD_COMPLETE
|
||||
assert out.current_state == PcState.BUILD_AWAIT_LEARNING
|
||||
assert dify.published is True
|
||||
|
||||
# 8) skip_learning -> build.complete
|
||||
out = runner.advance(s.id, Turn(action=Action(kind="skip_learning", base_version=out.version), actor=_actor()))
|
||||
assert out.current_state == PcState.BUILD_COMPLETE
|
||||
|
||||
# ordered card stream: every Build card kind appears, seq-ordered.
|
||||
items = repo.list_conversation(s.id)
|
||||
kinds = [i.kind for i in items]
|
||||
@@ -470,6 +553,9 @@ def test_full_build_flow_keep_draft_reaches_complete_without_publish():
|
||||
assert out.current_state == PcState.BUILD_REVIEW
|
||||
|
||||
out = runner.advance(s.id, Turn(action=Action(kind="keep_draft", base_version=out.version), actor=_actor()))
|
||||
assert out.current_state == PcState.BUILD_AWAIT_LEARNING # default policy "ask"
|
||||
|
||||
out = runner.advance(s.id, Turn(action=Action(kind="skip_learning", base_version=out.version), actor=_actor()))
|
||||
assert out.current_state == PcState.BUILD_COMPLETE
|
||||
assert dify.published is False # keep_draft skips publish
|
||||
assert not any(i.kind == "publish" for i in repo.list_conversation(s.id))
|
||||
|
||||
Reference in new issue
Block a user