Skip to content

Commit 2fa56b1

Browse files
committed
refactor(deployments): move flow-version tool_name into provider_data
Move provider tool_name from a top-level flow-version response field into provider_data, aligning API ownership boundaries for provider-originated non-persisted fields. - Remove top-level tool_name from DeploymentFlowVersionListItem - Add tool_name to WatsonxApiDeploymentFlowVersionItemData with normalization - Update WXO mapper to shape tool_name under provider_data - Update frontend attachments type and consumer to read provider_data.tool_name - Update backend tests for new response contract location - Add explicit RULES.md requirement for non-persisted provider data placement
1 parent 2d9c716 commit 2fa56b1

8 files changed

Lines changed: 72 additions & 48 deletions

File tree

src/backend/base/langflow/api/v1/mappers/deployments/RULES.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ Use this checklist before merge:
412412
- [ ] Provider-account update logic lives in mapper, not in route conditionals
413413
- [ ] Provider-specific cross-field rules (e.g. tenant/URL coupling) are implemented as mapper overrides calling `super()`, not as base-class conditionals
414414
- [ ] Credential extraction uses `resolve_credential_fields`, not route-level assumptions about `provider_data` contents
415+
- [ ] Non-persisted provider-originated fields are placed in `provider_data` (never top-level)
415416
- [ ] DB-level consistency validators exist as defense-in-depth for cross-field invariants
416417
- [ ] Tests cover both base mapper defaults and provider overrides
417418
- [ ] Failure cases for missing/unexpected bindings are covered
@@ -556,3 +557,14 @@ When adding a new field to an execution or deployment response:
556557
1. Is Langflow the source of truth for this value? → top level.
557558
2. Does this value come from the provider and Langflow just relays it? → inside `provider_data`.
558559
3. Does the provider supply it but Langflow persists and indexes it (like `resource_key`)? → top level is acceptable.
560+
561+
### 14.4 Hard placement rule for non-persisted provider data
562+
563+
If data is not persisted in the Langflow DB and comes directly from the provider,
564+
it must go into `provider_data`.
565+
566+
Rules:
567+
568+
- Top-level response fields are reserved for values that Langflow persists and controls.
569+
- Provider-originated data that Langflow only relays must stay in `provider_data` without exception.
570+
- Examples: provider tool names, execution metadata/status/timestamps, connection types, environments.

src/backend/base/langflow/api/v1/mappers/deployments/watsonx_orchestrate/mapper.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,8 +1093,10 @@ def shape_flow_version_list_result(
10931093
version_number=row.flow_version.version_number,
10941094
attached_at=row.attachment.created_at,
10951095
provider_snapshot_id=row.snapshot_id,
1096-
tool_name=snapshot_name_by_id.get(row.snapshot_id),
1097-
provider_data=self.shape_deployment_flow_version_item_data(snapshot_data_by_id.get(row.snapshot_id)),
1096+
provider_data=self.shape_deployment_flow_version_item_data(
1097+
snapshot_data=snapshot_data_by_id.get(row.snapshot_id),
1098+
tool_name=snapshot_name_by_id.get(row.snapshot_id),
1099+
),
10981100
)
10991101
for row in normalized_rows
11001102
]
@@ -1159,12 +1161,13 @@ def _resolve_snapshot_name_by_id(
11591161
11601162
Edge cases:
11611163
- Provider unreachable / snapshot_result is None: returns ``{}``.
1162-
The ``tool_name`` field in the response will be ``None`` and the
1163-
frontend falls back to the Langflow flow name for display.
1164+
``provider_data.tool_name`` will be absent/``None`` and the frontend
1165+
falls back to the Langflow flow name for display.
11641166
- Tool renamed in wxO console: the new name is returned here since
11651167
``snapshot_result`` is fetched fresh on each request.
11661168
- Tool deleted in wxO: missing from ``snapshot_result.snapshots``,
1167-
so no entry in the returned dict. ``tool_name`` will be ``None``.
1169+
so no entry in the returned dict. ``provider_data.tool_name`` will be
1170+
absent/``None``.
11681171
"""
11691172
if not snapshot_result or not snapshot_result.snapshots:
11701173
return {}
@@ -1178,17 +1181,21 @@ def _resolve_snapshot_name_by_id(
11781181

11791182
def shape_deployment_flow_version_item_data(
11801183
self,
1184+
*,
11811185
snapshot_data: dict[str, Any] | None,
1186+
tool_name: str | None = None,
11821187
) -> dict[str, Any] | None:
1183-
if not snapshot_data:
1184-
return None
1185-
raw_connections = snapshot_data.get("connections")
1186-
if raw_connections is None or not isinstance(raw_connections, dict):
1188+
raw_connections = snapshot_data.get("connections") if snapshot_data else None
1189+
app_ids = list(raw_connections.keys()) if isinstance(raw_connections, dict) else []
1190+
if not app_ids and not tool_name:
11871191
return None
11881192
try:
11891193
return self._validate_slot(
11901194
self.api_payloads.deployment_item_data,
1191-
{"app_ids": list(raw_connections.keys())},
1195+
{
1196+
"app_ids": app_ids,
1197+
"tool_name": tool_name,
1198+
},
11921199
)
11931200
except AdapterPayloadValidationError as exc:
11941201
detail = exc.format_first_error()

src/backend/base/langflow/api/v1/mappers/deployments/watsonx_orchestrate/payloads.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ class WatsonxApiDeploymentFlowVersionItemData(BaseModel):
527527
model_config = {"extra": "forbid"}
528528

529529
app_ids: list[str] = Field(default_factory=list)
530+
tool_name: str | None = None
530531

531532
@field_validator("app_ids", mode="before")
532533
@classmethod
@@ -535,6 +536,12 @@ def normalize_app_ids(cls, value: Any) -> list[str]:
535536
return []
536537
return [str(app_id).strip() for app_id in value if str(app_id).strip()]
537538

539+
@field_validator("tool_name", mode="before")
540+
@classmethod
541+
def normalize_optional_tool_name(cls, value: Any) -> str | None:
542+
normalized = str(value or "").strip()
543+
return normalized or None
544+
538545

539546
class _WatsonxApiAgentExecutionResultBase(BaseModel):
540547
"""Shared fields for API-facing agent execution result payloads.

src/backend/base/langflow/api/v1/schemas/deployments.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -331,15 +331,15 @@ class DeploymentFlowVersionListItem(BaseModel):
331331
distinguishes the following cases:
332332
333333
* **Tool renamed in provider** — Same ``provider_snapshot_id``, different
334-
``tool_name``. Langflow picks up the new name on the next fetch.
334+
``provider_data.tool_name``. Langflow picks up the new name on the next fetch.
335335
* **Tool deleted in provider** — ``provider_snapshot_id`` no longer
336-
resolves. ``tool_name`` and ``provider_data`` will be ``None``.
336+
resolves. ``provider_data.tool_name`` may be missing/``None``.
337337
* **Tool deleted + new tool created with same name** — The new tool has
338338
a different ID. Langflow's attachment still points to the old
339339
(missing) ID. The new tool is invisible to Langflow until explicitly
340340
attached via an update operation.
341341
342-
Frontends should use ``tool_name`` for display and
342+
Frontends should use ``provider_data.tool_name`` for display and
343343
``provider_snapshot_id`` for identity / operations.
344344
"""
345345

@@ -358,15 +358,6 @@ class DeploymentFlowVersionListItem(BaseModel):
358358
default=None,
359359
description="Provider-owned snapshot/tool identifier linked by the attachment.",
360360
)
361-
tool_name: str | None = Field(
362-
default=None,
363-
description=(
364-
"Provider tool name for this snapshot. May differ from ``flow_name`` "
365-
"if the user set a custom name at deploy time or renamed the tool in "
366-
"the provider console. ``None`` when the provider is unreachable or "
367-
"the tool has been deleted — frontends should fall back to ``flow_name``."
368-
),
369-
)
370361
provider_data: dict[str, Any] | None = Field(
371362
default=None,
372363
description="Provider-owned opaque payload for this attached flow version.",

src/backend/tests/unit/api/v1/test_deployment_mapper_watsonx.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,18 +229,25 @@ def test_watsonx_mapper_formats_conflict_detail(raw_message: str, expected: str)
229229
def test_watsonx_mapper_shapes_flow_version_item_data_from_connections() -> None:
230230
mapper = WatsonxOrchestrateDeploymentMapper()
231231

232-
shaped = mapper.shape_deployment_flow_version_item_data({"connections": {"cfg-1": "conn-1", "cfg-2": "conn-2"}})
232+
shaped = mapper.shape_deployment_flow_version_item_data(
233+
snapshot_data={"connections": {"cfg-1": "conn-1", "cfg-2": "conn-2"}},
234+
tool_name="Tool 1",
235+
)
233236

234-
assert shaped == {"app_ids": ["cfg-1", "cfg-2"]}
237+
assert shaped == {"app_ids": ["cfg-1", "cfg-2"], "tool_name": "Tool 1"}
235238

236239

237240
def test_watsonx_mapper_flow_version_item_data_handles_missing_invalid_and_empty_connections() -> None:
238241
mapper = WatsonxOrchestrateDeploymentMapper()
239242

240-
assert mapper.shape_deployment_flow_version_item_data(None) is None
241-
assert mapper.shape_deployment_flow_version_item_data({}) is None
242-
assert mapper.shape_deployment_flow_version_item_data({"connections": []}) is None
243-
assert mapper.shape_deployment_flow_version_item_data({"connections": {}}) == {"app_ids": []}
243+
assert mapper.shape_deployment_flow_version_item_data(snapshot_data=None) is None
244+
assert mapper.shape_deployment_flow_version_item_data(snapshot_data={}) is None
245+
assert mapper.shape_deployment_flow_version_item_data(snapshot_data={"connections": []}) is None
246+
assert mapper.shape_deployment_flow_version_item_data(snapshot_data={"connections": {}}) is None
247+
assert mapper.shape_deployment_flow_version_item_data(snapshot_data=None, tool_name="Tool 1") == {
248+
"app_ids": [],
249+
"tool_name": "Tool 1",
250+
}
244251

245252

246253
def test_watsonx_mapper_shapes_flow_version_list_result_with_enrichment() -> None:
@@ -282,7 +289,7 @@ def test_watsonx_mapper_shapes_flow_version_list_result_with_enrichment() -> Non
282289
assert shaped.flow_versions[0].version_number == 3
283290
assert shaped.flow_versions[0].attached_at == attached_at
284291
assert shaped.flow_versions[0].provider_snapshot_id == "tool-1"
285-
assert shaped.flow_versions[0].provider_data == {"app_ids": ["cfg-1"]}
292+
assert shaped.flow_versions[0].provider_data == {"app_ids": ["cfg-1"], "tool_name": "Tool 1"}
286293

287294

288295
def test_watsonx_mapper_flow_version_list_result_returns_empty_app_ids_when_snapshot_has_no_connections() -> None:
@@ -315,7 +322,7 @@ def test_watsonx_mapper_flow_version_list_result_returns_empty_app_ids_when_snap
315322
assert shaped.total == 1
316323
assert len(shaped.flow_versions) == 1
317324
assert shaped.flow_versions[0].provider_snapshot_id == "tool-1"
318-
assert shaped.flow_versions[0].provider_data == {"app_ids": []}
325+
assert shaped.flow_versions[0].provider_data == {"app_ids": [], "tool_name": "Tool 1"}
319326

320327

321328
def test_watsonx_mapper_flow_version_list_result_degrades_when_snapshot_result_missing() -> None:

src/backend/tests/unit/services/deployment/test_watsonx_orchestrate.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7112,33 +7112,32 @@ def test_rename_tool_provider_payload_parses():
71127112

71137113

71147114
# ---------------------------------------------------------------------------
7115-
# DeploymentFlowVersionListItem includes tool_name
7115+
# DeploymentFlowVersionListItem carries provider tool_name under provider_data
71167116
# ---------------------------------------------------------------------------
71177117

71187118

7119-
def test_flow_version_list_item_includes_tool_name():
7120-
"""DeploymentFlowVersionListItem accepts and serializes tool_name."""
7119+
def test_flow_version_list_item_includes_tool_name_in_provider_data():
7120+
"""DeploymentFlowVersionListItem serializes provider tool_name under provider_data."""
71217121
from langflow.api.v1.schemas.deployments import DeploymentFlowVersionListItem
71227122

71237123
item = DeploymentFlowVersionListItem(
71247124
id="00000000-0000-0000-0000-000000000001",
71257125
flow_id="00000000-0000-0000-0000-000000000002",
71267126
flow_name="My Flow",
71277127
version_number=1,
7128-
tool_name="my_custom_tool",
7128+
provider_data={"tool_name": "my_custom_tool"},
71297129
)
7130-
assert item.tool_name == "my_custom_tool"
71317130
data = item.model_dump()
7132-
assert data["tool_name"] == "my_custom_tool"
7131+
assert data["provider_data"]["tool_name"] == "my_custom_tool"
71337132

71347133

7135-
def test_flow_version_list_item_tool_name_defaults_to_none():
7136-
"""DeploymentFlowVersionListItem defaults tool_name to None."""
7134+
def test_flow_version_list_item_provider_data_defaults_to_none():
7135+
"""DeploymentFlowVersionListItem defaults provider_data to None."""
71377136
from langflow.api.v1.schemas.deployments import DeploymentFlowVersionListItem
71387137

71397138
item = DeploymentFlowVersionListItem(
71407139
id="00000000-0000-0000-0000-000000000001",
71417140
flow_id="00000000-0000-0000-0000-000000000002",
71427141
version_number=1,
71437142
)
7144-
assert item.tool_name is None
7143+
assert item.provider_data is None

src/frontend/src/controllers/API/queries/deployments/use-get-deployment-attachments.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import { UseRequestProcessor } from "../../services/request-processor";
88
*
99
* Identity contract: Langflow tracks provider tools by their immutable
1010
* `provider_snapshot_id` (wxO tool_id), never by name.
11-
* - Tool renamed in provider → same snapshot ID, new `tool_name`.
12-
* - Tool deleted in provider → snapshot ID unresolvable, `tool_name` is null.
11+
* - Tool renamed in provider → same snapshot ID, new `provider_data.tool_name`.
12+
* - Tool deleted in provider → snapshot ID unresolvable, `provider_data.tool_name` is null/missing.
1313
* - Tool deleted + new tool created with same name → different ID, our
1414
* attachment still points to the old (missing) ID. The new tool is
1515
* invisible to Langflow until explicitly attached.
1616
*
17-
* Use `tool_name` for display, fall back to `flow_name` when null.
17+
* Use `provider_data.tool_name` for display, fall back to `flow_name` when null.
1818
* Use `provider_snapshot_id` for operations.
1919
*/
2020
export interface DeploymentFlowVersionItem {
@@ -24,10 +24,10 @@ export interface DeploymentFlowVersionItem {
2424
version_number: number;
2525
attached_at: string | null;
2626
provider_snapshot_id: string | null;
27-
/** Provider tool name — null when the tool was deleted or provider is unreachable. */
28-
tool_name: string | null;
2927
provider_data: {
3028
app_ids?: string[];
29+
/** Provider tool name — null/missing when the tool was deleted or provider is unreachable. */
30+
tool_name?: string | null;
3131
} | null;
3232
}
3333

src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {
88
DialogTitle,
99
} from "@/components/ui/dialog";
1010
import { usePostProviderAccount } from "@/controllers/API/queries/deployment-provider-accounts/use-post-provider-account";
11-
import { useGetDeploymentAttachments } from "@/controllers/API/queries/deployments/use-get-deployment-attachments";
1211
import { useGetDeployment } from "@/controllers/API/queries/deployments/use-get-deployment";
12+
import { useGetDeploymentAttachments } from "@/controllers/API/queries/deployments/use-get-deployment-attachments";
1313
import { usePatchDeployment } from "@/controllers/API/queries/deployments/use-patch-deployment";
1414
import { usePostDeployment } from "@/controllers/API/queries/deployments/use-post-deployment";
1515
import {
@@ -71,7 +71,7 @@ export default function DeploymentStepperModal({
7171
//
7272
// - If a user renames a tool in the wxO console, the new name appears
7373
// here on the next edit. Langflow doesn't cache tool names locally.
74-
// - If a tool is deleted in wxO, its tool_name will be null and the
74+
// - If a tool is deleted in wxO, provider_data.tool_name will be null and the
7575
// review page falls back to the Langflow flow name.
7676
// - If a connection is deleted in wxO but the tool still references it,
7777
// the app_id will appear in connectionsByFlow. The backend will fail
@@ -93,8 +93,9 @@ export default function DeploymentStepperModal({
9393
versionTag: `v${fv.version_number}`,
9494
});
9595
// Pre-populate tool names from the provider (may differ from flow name).
96-
if (fv.tool_name) {
97-
toolNames.set(fv.flow_id, fv.tool_name);
96+
const providerToolName = fv.provider_data?.tool_name;
97+
if (providerToolName) {
98+
toolNames.set(fv.flow_id, providerToolName);
9899
}
99100
// Pre-populate attached connections from existing tool bindings.
100101
const appIds = fv.provider_data?.app_ids;

0 commit comments

Comments
 (0)