Commit Graph
59 Commits
Author SHA1 Message Date
Charles YaoandClaude Opus 5 fe3f8154b5 feat(dify-builder): carry node usage rules on the Builder-only directive
Meeting item 9 asked for per-node usage detail in the generator prompts.
Those prompts are shared with cmd+K, so the detail rides the channel the
Builder already owns instead: _WORKFLOW_TOPOLOGY_DIRECTIVE, prepended to
every generator instruction at build.py:377 and invisible to cmd+K. That
mechanism is proven -- it is why workflow mode never gets 'answer' nodes.

The rules added are the ones that broke real builds: tool nodes expose
text/files/json and not 'result' (ESQ1-296, ESQ1-289), document-extractor
needs a file input, container children belong inside the container
(ESQ1-288), if-else operators must match the variable's type
(ESQ1-285/290), and http-request inputs belong on the start node
(ESQ1-286).

Deliberately does NOT restate the node whitelist: the generator's shared
system prompt already carries it, and duplicating it would burn planner
output budget that ESQ1-300 already implicates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 23:08:27 -07:00
Charles YaoandClaude Opus 5 f12d8f4e0f feat(dify-builder): plan in Dify node vocabulary, not business language
propose_plan_v1's system prompt was two sentences with no node vocabulary,
so it planned in business language -- "Send a Feishu notification",
"Connect to the database". Those steps became the generator's
"# User instruction" verbatim, asking it to map work onto nodes that do
not exist, and the failure only surfaced at build-generate-graph, minutes
after the user had approved the plan (ESQ1-279).

The generator already knows the node types: its SHARED system prompt
carries the whitelist. The gap was upstream, in the Builder's own planner.
Naming the vocabulary here makes that existing whitelist sufficient.

Builder-only: nothing under core/workflow/generator/ is touched, so the
cmd+K /create and /refine features are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 23:05:34 -07:00
Charles YaoandClaude Opus 5 674669f7a6 fix(dify-builder): carry the container start marker's ReactFlow type
The previous ESQ1-288 fix dropped the iteration/loop start marker, stripped
start_node_id from the container config and dropped the generator's container
entry edge. All three are required by the ENGINE, not just the canvas:
IterationNodeData.start_node_id / LoopNodeData.start_node_id have no default,
node data is validated eagerly at graph construction, and the body frame is
rooted at start_node_id. Every container workflow the Builder produced was
therefore non-runnable -- worse than the canvas crash it replaced.

The real root cause was never the marker node. The generator already emits it
with the correct node-level ReactFlow type ("custom-iteration-start" /
"custom-loop-start", runner.py:1350-1362); _build_node hardcoded "type":
"custom" and threw it away, leaving a node the frontend has no component for
(React error #130).

Carry it through instead, the same way the previous commit carried
parentId/position: a defaulted `flow_type` on _build_node / apply_create_node
(absent -> "custom", so every existing caller is byte-identical), set from the
source node's type in to_intents. The marker, start_node_id and the entry edge
are all persisted again.

Tests: the three tests that asserted the ABSENCE of the marker, start_node_id
and the entry edge are replaced by ones asserting their presence, plus the
round-trip test that would have caught this -- to_intents -> filter_applicable
-> APPLY_FNS -> IterationNodeData/LoopNodeData.model_validate on the persisted
container.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 22:09:26 -07:00
Charles YaoandClaude Opus 5 f0c7b7c413 feat(dify-builder): carry nesting and layout through translation
to_intents read only id and data, so every node-level ReactFlow key --
parentId, extent, zIndex, position -- was dropped on each Builder build.
Iteration and loop children were written un-nested, not inside their
container at all, and the generator's computed layout was replaced by
default positions. The data-level markers (isInIteration, iteration_id)
survived via config, which is why the graphs looked almost right.

apply_create_node already took position; parent_id is new and implies
extent="parent" + zIndex=1002, matching what the generator computes.

Unfiled; found while tracing ESQ1-288. Skipping the synthetic start node
stops the canvas crash, but an iteration whose children are not inside it
still does not work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 22:09:26 -07:00
Charles YaoandClaude Opus 5 8ceee4d3be fix(dify-builder): skip synthetic nodes and clean up dangling references
Builder-generated iterations/loops crashed the canvas permanently with
React error #130 or TypeError. The backend synthesizes container-start
markers correctly typed, but to_intents was recreating them as generic
nodes, and persisting dangling start_node_id pointers.

Fix three related issues:
1. Skip creating synthetic iteration-start/loop-start nodes (frontend
   regenerates them at render time from the container).
2. Strip start_node_id from container configs; the frontend's
   workflow-init.ts rewrites it to match the marker it creates, and
   persisting our pointer causes TypeError on dereference without guard.
3. Skip edges whose source or target is a skipped node; the generator
   emits entry edges to the synthetic marker, which no longer exist.

The generator's own output (cmd+K) is untouched — only the Builder
translation layer is fixed. Zero blast radius.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 22:09:26 -07:00
Charles YaoandClaude Opus 5 69ccf2ad15 fix(dify-builder): wire resource readiness into recommender/gap prompts
Three minor whole-branch-review fixups:

- ResourceOption.readiness ("ready"/"missing_config") was computed but never
  shown to the model: discover_resources's recommender prompt listed ids and
  labels only, so it could recommend an unauthorized tool; _GAP_SYSTEM showed
  readiness in the listing (assess_capability_gap) but never defined what
  missing_config means. Both prompts now show readiness and state that
  missing_config is installed-but-not-authorized, i.e. not usable -- pinned
  with tests asserting the literal "missing_config" in the constructed
  prompts.
- strings.py: dropped three orphaned catalogue entries ("Here's the initial
  plan.", "Ask each time", "Prefer audited") -- the conflict-policy concept
  they backed was removed end to end; nothing in the handlers emits them.
- core/workflow/generator/tool_catalogue.py: provider.need_credentials was
  read OUTSIDE the per-provider try/except that already guards
  get_tools()/tool parsing. This module is shared with the cmd+K /create and
  /refine flows, so a provider whose property raised would break those too,
  not just the Builder. Moved the read inside the try/except so a bad
  provider is skipped like any other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 16:41:52 -07:00
Charles YaoandClaude Opus 5 f1bf2fb318 feat(dify-builder): say when the workspace cannot cover the plan
Add assess_capability_gap: one plain sentence naming a plan step nothing
installed can perform, or "" when the plan is covered. Grounded only in
installed resources -- there is no marketplace lookup, so it names a missing
CAPABILITY, never a product to install. A missing_config tool (installed but
unauthorized, from the prior task's real readiness) doesn't count as
covering a step either, since the model is shown each option's readiness
alongside its label and kind.

Wired through the full agent seam: the DifyBuilderAgent protocol, the real
LlmBuilderAgent (delegates to build.assess_capability_gap), and the
PlaceholderAgent test double (returns ""). handlers_build.py calls it
after discover_resources and, when non-empty, appends a NoticeItem beside
the existing empty-resources notice -- the two are independent and can
both fire.

Like its sibling cognition functions, it degrades to "" (never raises) on
no model, an LLM error, or an unparseable reply: a gap that can't be
assessed isn't a gap worth inventing. The sentence is LLM-generated prose
already in the user's language (json_language_instruction), not a
catalogue entry, so no static string was added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 16:13:02 -07:00
Charles YaoandClaude Opus 5 0a228b7c23 feat(dify-builder): report whether a recommended tool is actually authorized
The Builder recommended every installed tool as ready to use without
ever checking whether its provider actually has credentials configured.
A credentialed tool with no tool_builtin_providers row got bound into a
plan and then failed at test time with an authorization error the user
can't act on.

tool_catalogue.py now carries provider_controller.need_credentials onto
each ToolCatalogueEntry as needs_credentials. resources.py resolves
which providers are actually authorized for the tenant (mirroring
ToolTransformService.builtin_provider_to_user_provider's need_credentials
/ db_provider rule exactly, so the Builder and the console's tools page
can never disagree) and sets ResourceRef.readiness to "missing_config"
for a credentialed tool with no authorized provider row, "ready"
otherwise. Datasets and models are unaffected. If the authorization
query itself fails, readiness fails open to "ready" rather than
mass-labeling every credentialed tool unauthorized on a transient error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 16:04:48 -07:00
Charles YaoandClaude Opus 5 e8b14b50ff refactor(dify-builder): drop the resource conflict policy
The resource-recommendation step made the user pick a "conflict
policy" that never decided anything. Remove it end to end: the
ConflictPolicyOption card option, ResourceSelectCard's
conflict_policy_options field, the third bind_resources() parameter
across the port/placeholder/LLM-agent implementations, and the dead
conflict_policy field on the (unused) ResourcesPayload contract type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:10:34 -07:00
Charles YaoandClaude Opus 5 d3087cbe34 fix(dify-builder): stop recommending the whole workspace when nothing matches
discover_resources initialised its selection to the entire tenant inventory and
only replaced it when the model returned a non-empty pick, so three paths -- no
model, a model that picked nothing, a failed call -- recommended everything. On
a workspace with 57 resources that is not merely noisy: the client pre-checks
every recommendation and submits them all by default, and bind_resources then
appends all 57 labels to the last plan item, which is the text fed to the graph
generator.

Recommending nothing is the honest answer when there is no signal. The card is
still emitted so the confirm gate keeps its active interaction and a later pass
cannot inherit a stale one, and a notice says why rather than leaving a blank
card. The conflict-policy control no longer renders over an empty list.

The test that should have caught this used a one-resource catalog, where "all"
and "none" are indistinguishable; it now uses 57.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:09:18 -07:00
Charles YaoandClaude Opus 5 aa2b4fe4ab feat(dify-builder): name apps from their prompt, and turn gate actions into card options
Two App Builder spec changes that share the Builder session contract.

Naming (spec N1-N3): an app created from the homepage prompt is no longer named
by the user. core/dify_builder/naming.py cuts the prompt's opening clause at
create time -- instant and infallible, since it runs inside POST /apps -- and
CreateAppPayload.name becomes optional alongside a new prompt field. The Builder
then refines that name once during goal analysis, where a model and the detected
reply language are already in hand, so the judgement calls that are
English-shaped in pure Python (dropping an imperative lead-in, spotting a
contentless prompt) work in any language. Plan approval freezes the name.

Card options: SessionView gains a Decision carrying the gate's choice as options
inside the card plus a fixed confirm/cancel pair, replacing a button row whose
shape changed at every state. Option ids are the old action ids untouched, so
resolve_action_kind and the per-state legality check are unchanged, and
SessionView.actions keeps being populated -- the deployed web client goes on
working until it reads decision instead.

The regenerated console OpenAPI doc also picks up ErrorCard.diagnostics, missed
when cc9ca8b1dd landed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 17:55:38 -07:00
Charles YaoandClaude Opus 5 cc9ca8b1dd feat(dify-builder): carry generation diagnostics into the exported debug log
api pods lose their logs on restart, so a failed build's real cause
("Workflow generator: structural validation failed: ...") could only be read
from a pod log that may already be gone. Session 01a08fd0 hit exactly that.

Carry the generator's own diagnostics back with the result instead:
BuildNodesResult and ErrorCard gain a ``diagnostics`` list, and build_nodes
records one entry per failed generation attempt -- initial AND the corrective
retry, so an error that changes between attempts stays visible -- plus the
all-rejected and exception paths.

Each entry holds a server-side UTC ``at`` (so the surviving pod-log window can
be searched around it), ``source``, ``attempt``, the generator's ``message``
(the same text the server logs), its ``codes``, and the full structured
``errors`` including ``node_id``. That is strictly more than the log line
carries: for an UNRESOLVED_REFERENCE the export now names the offending node.

The list rides in the ErrorCard's item payload, which is what the streamed
conversation item -- and therefore the exported debug log -- preserves. It is
not rendered in the UI. Retries that eventually succeed keep their breadcrumbs.

Deliberately not mirroring the "structural validation failed" prefix: build.py
cannot tell structural failures from the rest without duplicating the
generator's code taxonomy, which would go stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:16:10 -07:00
twwu cecf2a1b9a fix(dify-builder): handle unavailable models during session creation 2026-09-11 18:28:52 +08:00
Charles YaoandClaude Opus 4.8 e59389776d fix(dify-builder): surface the specific reason in build-fail error card
When build-generate-graph failed at plan-approval, the error card always
showed one hardcoded generic message regardless of the actual cause
(UNRESOLVED_REFERENCE, non-object JSON, credit_balance_exhausted, ...).

Root cause: build_nodes() returned only list[MutationIntent]; on failure it
returned [], so the reason was logged internally but never reached the
handler, which had nothing to show but a fallback string.

Thread the reason out: build_nodes now returns BuildNodesResult{intents,
error}. _generation_error_text() extracts the best reason (top-level error
-> joined errors[].detail/message/code -> generic fallback), and every
failure path populates .error (generator error, all-intents-rejected, and
the except path via str(exc) so provider errors surface verbatim).
handle_plan_approval builds the ErrorCard body from build_result.error when
present, keeping the generic sentence only as a genuine fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 21:00:47 -07:00
Charles YaoandClaude Opus 4.8 2a574c7f74 fix(dify-builder): ground built workflow nodes to the user-selected model
Build wrote the Builder's session/cognition model (e.g. deepseek) into every
generated node's model block, ignoring the model the user picked at resource
confirmation (e.g. gpt). _ground always used the generation model, and the
selected model resource only reached the plan as label text (bind_resources) —
so a workflow got a model with no runtime credentials -> ModelNotExistError ->
instant test failure.

Thread the selected resource_ids from resource_selection through
agent.build_nodes into build.build_nodes, resolve the selected MODEL resource
(id is `{provider}/{name}`) via _selected_workflow_model, and ground node model
blocks to it — falling back to the session/generator model when the user
selected no model. The generator still runs on the session model (cognition);
only the built workflow's runtime model follows the user's choice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 19:18:36 -07:00
Charles YaoandClaude Opus 4.8 70c92b97d9 fix(dify-builder): make M2 translation parsing model-agnostic
Harden the Localizer's batch translation against models that format JSON
differently (the Builder model is user-changeable):
- correlate by numeric INDEX instead of echoing source strings as keys, so a
  model that normalizes/truncates long keys no longer misses every lookup
- accept the reply as an index-keyed object OR a positional array, optionally
  wrapped one level (_translations_by_index)
- cache only real translations; a failed/partial reply is left UNCACHED so the
  next turn retries (self-heals) instead of sticking on the English original

TDD: added parser shape-variation tests + a no-cache-failure/self-heal test;
the fake now speaks the index protocol.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 17:31:38 -07:00
Charles YaoandClaude Opus 4.8 28fc7d0fac fix(dify-builder): unwrap the model's translation envelope in the Localizer
Live e2e revealed M2 never translated the static catalog strings: the model
returns the batch-translate mapping wrapped under a "strings" key (echoing the
request envelope) — {"strings": {src: translation}} — so _fill_cache's
top-level table.get(src) missed every source and cached the English original.
Unwrap one level when the sources aren't top-level keys, and tighten the prompt
to ask for a flat object. The unit-test fake now mirrors the real wrapped shape
so this can't regress (a flat fake hid the bug).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 17:03:29 -07:00
Charles YaoandClaude Opus 4.8 9b4e095e71 refactor(dify-builder): promote model_or_none to the agent protocol
Follow-up to the reply-language feature. `_model_or_none` was private to
LlmBuilderAgent, so the advance-task wiring reached it via a fragile
`getattr(agent, "_model_or_none", lambda: None)`. Make it part of the
DifyBuilderAgent protocol (typed `object | None` to keep the pure port free of
any model-runtime import) and implement it on every conforming agent
(LlmBuilderAgent renames the private method public; PlaceholderAgent and the
test doubles return None). The Localizer wiring now uses `agent.model_or_none`
directly. Also drops the useless-return ruff nit in the wiring test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 16:34:29 -07:00
Charles YaoandClaude Opus 4.8 ad06a30961 fix(dify-builder): restore reply_language persistence, share translation cache, harden localize contract
Final-review fix wave for the "reply in the user's input language" feature:

- serde.py: context_from_dict omitted reply_language from the explicit field
  allowlist, so it round-tripped to "" on every read despite context_to_dict
  (asdict) serializing it -- the localization feature silently no-op'd after
  the first persisted write. Added it to the constructor call; TDD'd with a
  failing-then-passing round-trip test in test_serde.py.
- localize.py: Localizer._cache was per-instance, but a fresh Localizer is
  built per advance-task invocation, so the static string catalog was
  re-translated every turn instead of once per language. Moved the cache to a
  module-level _TRANSLATION_CACHE dict (safe to share: keys are
  tenant/session-independent catalog-string+language pairs); added an autouse
  fixture in test_localize.py to clear it between tests for isolation.
- localize.py: detect_language and _fill_cache called self._model_provider()
  outside their try/except, violating the Env callback contract that these
  must never raise. Moved both calls inside their try/except so a raising
  provider degrades to the existing safe fallback instead of propagating into
  advance/_commit.
- strings.py: documented that Template.translate_fields is advisory only --
  the Localizer re-inserts all captured groups verbatim and does not consult
  it. No behavior change.

uv run --project api pytest tests/unit_tests/core/dify_builder tests/unit_tests/services/dify_builder -q
-> 722 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 16:17:54 -07:00
Charles YaoandClaude Opus 4.8 7e70c7e189 fix(dify-builder): validate detect_language reply shape before trusting it (M2 fix round 1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 15:33:11 -07:00
Charles YaoandClaude Opus 4.8 32870a731a feat(dify-builder): Localizer — detect language + cached catalog translation (M2)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 15:24:23 -07:00
Charles YaoandClaude Opus 4.8 9a87125231 feat(dify-builder): instruct agent to reply in the user's language (M1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 15:02:42 -07:00
Charles YaoandClaude Opus 4.8 97b8354173 fix(dify-builder): capture verify-launch errors instead of swallowing them
The build/edit test handlers wrapped run_draft in a bare, non-logging
`except Exception` that turned any launch-time failure into a blank failed
run (dify_run_id="", per_node=[], no error). Diagnose then had nothing to
act on and the repair loop ran blind — the prod symptom where a stale test
input (ValueError "<var> is required in input form") produced an
undiagnosable await_repair loop.

Now the launch error is logged, captured, and persisted so diagnose/routing
can act on it:
- Run.error field + dify_builder_runs.error column (migration a9cc8d552db4)
- handlers_build/edit: logger.exception + launch_error_text(exc) -> Run.error
- is_input_failure() also inspects Run.error (+ "in input form" signal), so a
  stale-input launch failure routes to the testdata gate, not blind repair
- diagnose/_degraded_diagnosis surface the launch error when no node executed

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-10 14:24:51 -07:00
twwu ac7db51302 fix(dify-builder): align form schema types with values 2026-09-04 16:10:22 +08:00
twwu 4893c5e621 refactor(dify-builder): separate reasoning from execution progress 2026-09-04 13:43:16 +08:00
twwu eb25c2d8ee feat(dify-builder): add live progress and conversation streaming 2026-09-04 10:16:50 +08:00
twwu 3eacff14a6 feat: add typed SSE interactions to Dify Builder 2026-09-03 08:48:23 +08:00
Charles YaoandClaude Opus 4.8 e7e14ab888 fix(dify_builder): stop empty/mode-mismatched builds silently emptying the canvas
A build for a workflow app whose goal reads like a chatbot produced an empty
canvas while the assistant reported "Workflow built on the canvas.": the plan
was chatbot-shaped, build_nodes fed it to the generator (mode="workflow"), the
LLM emitted a chatflow graph (answer node / no end node), the generator
rejected it as MISSING_TERMINAL -- a class it does NOT retry -- and build_nodes
swallowed the error into []. handle_plan_approval then treated [] as a
successful build: it deleted the placeholder start and advanced to
build.execution, leaving nothing on the canvas.

Two parts:

1. build_nodes now prepends an explicit workflow-topology directive (one
   start + >=1 end node, no answer nodes) to the generator instruction, and
   retries ONCE on an error/empty result with the specific error fed back as
   a corrective nudge -- covering the terminal-node failure the shared
   generator skips.

2. handle_plan_approval refuses to fake success: when build_nodes yields zero
   create_node intents it surfaces an honest error and stays in
   build.plan_approval (retryable) without deleting the placeholder, instead
   of emptying the canvas and claiming a build. A loop-back re-approve still
   returns the fixed create intents (only filtered as already-present), so
   zero create intents unambiguously means a genuine generation failure.

Adds regression tests for both. Note: part 1 improves LLM reliability
(verifiable live); part 2 is the deterministic guarantee against the
false-success empty canvas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 14:39:56 -07:00
Charles YaoandClaude Opus 4.8 ba7692e2e9 feat(dify-builder): log build_nodes empty/failed generation and starved resource sources
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 10:03:48 -07:00
Charles YaoandClaude Opus 4.8 6f27a709ba feat(dify-builder): log llm-agent model-resolution failure once per advance
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 10:03:48 -07:00
Charles YaoandClaude Opus 4.8 133e8fdc0c refactor(dify-builder): remove unused propose_build_repair from agent Protocol
It was stubbed to [] in LlmBuilderAgent, canned in PlaceholderAgent, and never
called by any handler — the Build failure path reuses Fix cognition (diagnose +
propose_repair). Removed from the DifyBuilderAgent Protocol, both implementations,
and all test doubles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 10:03:48 -07:00
Charles YaoandClaude Opus 4.8 86341638df fix(dify-builder): keep generator node ids in graph_translate (no start rename)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 10:03:47 -07:00
Charles YaoandClaude Opus 4.8 8b640e44de Fix Dify Builder live-test escape paths to reset stale test inputs
Clear fc.test_input_ref and fc.verify_run_id on the four re-plan/revert
escape branches in Build and Edit (handle_review's continue_adjusting
and handle_reverted's retry_after_revert) so a rebuild+retest
regenerates fresh schema-shaped mock inputs instead of reusing the
first build's stale ones -- the approve_repair retest path is left
untouched since it must fairly retest against the same failing case.
Also corrects three doc/comment mismatches (llm_agent's 12->13 method
count, an await_repair docstring describing a branch that doesn't
exist, and state.py's Build state-count banner), and adds tests for
the run_draft-raises degrade path and Build/Edit pass-path canvas
event symmetry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 19:26:48 -07:00
Charles YaoandClaude Opus 4.8 bbbf2ff457 feat(dify-builder): real schema-shaped generate_mock_inputs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 18:01:16 -07:00
Charles YaoandClaude Opus 4.8 cd5fb6bbf0 fix(dify-builder): ground model on all model-bearing nodes, carry branch handles, clean test lint
Final consolidated fix wave from whole-branch review of the Real Build +
Edit Cognition slice:

- _ground now overwrites the model dict on ANY create_node whose config
  carries a model dict (llm, question-classifier, parameter-extractor),
  not just "llm", so a drifted/hallucinated provider/name on those other
  node types no longer flows through ungrounded.
- to_intents now carries a generator edge's sourceHandle/targetHandle
  into the connect intent's args (source_handle/target_handle) so
  branching nodes (if-else, question-classifier) wire onto the correct
  port instead of collapsing onto the default handle; edges without
  handles still produce bare {from_node, to_node} args.
- Cleared the 17 ruff errors (PT018/E302/E306/E501) across the slice's
  test files by splitting composite asserts, adding missing blank
  lines, and wrapping long lines -- no assertions weakened, no noqa.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 14:22:19 -07:00
Charles YaoandClaude Opus 4.8 a07dc0e766 fix(dify-builder): correct method count in llm_agent docstring
Docstring said "9 now use real LLM cognition" but enumerated 3 Fix +
6 Build + 3 Edit = 12 methods (Task B3 added the 3 Edit methods
without updating the leading count). Fix the count to 12 of 14 to
match the enumerated list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 14:06:25 -07:00
Charles YaoandClaude Opus 4.8 27be3de30a feat(dify-builder): wire real Edit cognition into LlmBuilderAgent
analyze_impact/propose_edit_plan/build_edit_intents now call
services.dify_builder.agent.edit.* via _model_or_none() instead of
delegating to the canned PlaceholderAgent. 9 of the Protocol's 14
Build/Edit/Fix methods now use real LLM cognition; only
generate_mock_inputs and propose_build_repair (returns []) remain
deferred pending the live-test step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 14:00:58 -07:00
Charles YaoandClaude Opus 4.8 f801481fdc fix(dify-builder): guard analyze_impact target_node_ids; drop ARG noqa in edit tests
analyze_impact could raise on a non-list target_node_ids from the LLM (no
isinstance guard, unlike fields/values) -- violated the never-crash
contract. Guard it the same way. Also collapse the fake models' invoke_llm
signatures to **_kwargs (underscore-prefixed, no noqa) per the ARG lint
constraint, and pin two more malformed-shape degrade paths
(build_edit_intents non-list intents, propose_edit_plan non-list plan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 13:54:08 -07:00
Charles YaoandClaude Opus 4.8 f5458e16b3 feat(dify-builder): real Edit cognition (impact/plan/intents via Fix pattern)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 13:41:59 -07:00
Charles YaoandClaude Opus 4.8 8683e6517c feat(dify-builder): wire real Build cognition + neutral empty test-repair
LlmBuilderAgent's 6 Build methods (analyze_goal, propose_plan_v1,
discover_resources, bind_resources, build_nodes, learn_from_build) now
delegate to services.dify_builder.agent.build instead of the canned
PlaceholderAgent. propose_build_repair returns [] -- there's no real
failure signal to diagnose until the live test step is de-canned.

handle_test_and_repair now conditions the error card + apply_repair on
a non-empty propose_build_repair result: placeholder mode keeps today's
"found and fixed" arc, while real cognition (which never reports a
failure yet) emits a neutral "no issues found" review summary instead
of a fake error/fix on a hardcoded node id that real build_nodes
wouldn't have generated anyway.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 12:25:14 -07:00
Charles YaoandClaude Opus 4.8 02edd73204 fix(dify-builder): stop fabricating llm completion_params, dedupe dataset_ids grounding
Grounding only overwrites provider+name on an llm node's model dict now,
preserving whatever mode/completion_params the generator produced instead
of synthesizing a temperature when none was present. knowledge-retrieval
grounding now assigns each node its own dataset_ids list copy instead of
aliasing one shared list across every KB node in the graph.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 12:15:08 -07:00
Charles YaoandClaude Opus 4.8 dc85ea51a2 feat(dify-builder): build_nodes via WorkflowGenerator with model+dataset grounding
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 12:04:58 -07:00
Charles YaoandClaude Opus 4.8 0ba1f932a9 fix(dify-builder): address review findings for build.py cognition
- Fix S110: replace bare except-pass with explicit degrade behavior
- Fix E501: wrap long function signatures to <=120 chars
- Fix I001: sort imports in test file (resources to top)
- Fix PT018: split composite assertions into separate statements
- Add _BoomInstance for provider-error testing
- Add 5 degrade tests (analyze_goal, propose_plan_v1 x2, discover_resources, learn_from_build)
- Remove unused _KIND_BY_PREFIX dict
- Add noqa comments for test mock arguments

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 11:50:40 -07:00
Charles YaoandClaude Opus 4.8 2e77396d39 feat(dify-builder): real Build light cognition (analyze/plan/discover/bind/learn)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 11:40:05 -07:00
Charles YaoandClaude Opus 4.8 bcd0f59100 fix(dify-builder): prevent duplicate start node ids on multiple starts
Only map the first start-type node to canonical 'start'; additional starts
retain their original ids to prevent collisions and ambiguous intents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 11:36:02 -07:00
Charles YaoandClaude Opus 4.8 4ec3e5bfe7 feat(dify-builder): translate generator graph into build intents
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 11:30:08 -07:00
Charles YaoandClaude Opus 4.8 a5f8d71eb7 fix(dify-builder): dataset visibility + normalization guard + test coverage
FINDING 1 (dataset visibility): DatasetService.get_datasets requires user
parameter and silently omits ONLY_ME/PARTIAL_TEAM datasets on RBAC tenants.
Query Dataset model directly by tenant_id for complete grounding inventory.

FINDING 2 (normalization guard): Move fetch+normalize inside _safe to prevent
malformed records from raising after _safe returns. Use lambda builders to wrap
complete resource transformation including record normalization.

FINDING 3 (test coverage): Add test_mixed_failure_partial_success (one source
fails, others succeed) and test_malformed_tool_record_degrades (missing
tool_name key gracefully degrades).

Minor: compute model id once and reuse for both id and label fields; ensure
E302 two blank lines between test functions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 11:24:56 -07:00
Charles YaoandClaude Opus 4.8 d976f9bf66 feat(dify-builder): tenant resource inventory (models/datasets/tools)
Implement resources.py with list_tenant_resources() to normalize tenant's
configured models, datasets, and tools to ResourceRef(id, label, meta,
readiness). Each source degrades to empty list on failure for graceful
degradation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 11:15:57 -07:00
Charles YaoandClaude Opus 4.8 e44ab9bb57 fix(dify-builder): broaden filter_applicable's except, fix stale docstring + typo
filter_applicable's per-intent try/except only caught (ValueError, TypeError),
so a hallucinated insert_between with a non-dict edge arg raised an uncaught
AttributeError and crashed the whole advance instead of being rejected by the
dry-run. Broaden to except Exception (ruff does not flag it, so no noqa is
added). Add a regression test for the string-edge case.

Also update llm_agent.py's stale module docstring (Fix cognition is now real,
not delegated; Protocol has 14 methods, not 13) and fix a stray closing paren
in a test_handlers_fix.py comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 02:39:08 -07:00
Charles YaoandClaude Opus 4.8 8a1f0382d3 feat(dify-builder): wire LlmBuilderAgent Fix methods to real cognition
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 02:12:46 -07:00