From 2a742486c5cbecf221bb35fe2df34c2425495cf1 Mon Sep 17 00:00:00 2001 From: chelsealong Date: Sat, 8 Aug 2026 14:55:27 +0800 Subject: [PATCH] fix(agent-v2): accept composer UI ids on manual metadata filter conditions (#40171) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../nodes/agent_v2/runtime_request_builder.py | 7 ++- api/models/agent_config_entities.py | 11 +++++ api/openapi/markdown/console-openapi.md | 10 +++++ .../agent_v2/test_runtime_request_builder.py | 8 +++- .../agent/test_agent_composer_entities.py | 45 +++++++++++++++++++ .../generated/api/console/agent/types.gen.ts | 2 + .../generated/api/console/agent/zod.gen.ts | 10 +++++ .../generated/api/console/apps/types.gen.ts | 2 + .../generated/api/console/apps/zod.gen.ts | 10 +++++ .../api/console/snippets/types.gen.ts | 2 + .../generated/api/console/snippets/zod.gen.ts | 10 +++++ 11 files changed, 115 insertions(+), 2 deletions(-) diff --git a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py index 126ce503f81..367dd2e6d42 100644 --- a/api/core/workflow/nodes/agent_v2/runtime_request_builder.py +++ b/api/core/workflow/nodes/agent_v2/runtime_request_builder.py @@ -836,7 +836,12 @@ def _knowledge_metadata_filtering_config( return DifyKnowledgeMetadataFilteringConfig( mode=metadata_filtering.mode, model_config=_knowledge_model_config(metadata_filtering.metadata_model_config), - conditions=cast(Any, metadata_filtering.conditions.model_dump(mode="json")) + conditions=cast( + Any, + metadata_filtering.conditions.model_dump( + mode="json", exclude={"conditions": {"__all__": {"id", "metadata_id"}}} + ), + ) if metadata_filtering.conditions is not None else None, ) diff --git a/api/models/agent_config_entities.py b/api/models/agent_config_entities.py index a6a0cd544a2..b919eaed602 100644 --- a/api/models/agent_config_entities.py +++ b/api/models/agent_config_entities.py @@ -420,8 +420,19 @@ class AgentKnowledgeRetrievalConfig(BaseModel): class AgentKnowledgeMetadataCondition(BaseModel): + """One manual metadata filter clause. + + ``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on + every save (a stable row key and a reference to the selected metadata + field). They are persisted here for round-tripping the composer's draft + state but are stripped before building the Agent runtime request, whose + DTO only accepts ``name``/``comparison_operator``/``value``. + """ + model_config = ConfigDict(extra="forbid") + id: str | None = None + metadata_id: str | None = None name: str = Field(min_length=1, max_length=255) comparison_operator: SupportedComparisonOperator value: ConditionValue = None diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index ef1a80c5ce2..2745eacd15f 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -14302,9 +14302,19 @@ the current roster/workflow APIs scoped to Dify Agent. #### AgentKnowledgeMetadataCondition +One manual metadata filter clause. + +``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on +every save (a stable row key and a reference to the selected metadata +field). They are persisted here for round-tripping the composer's draft +state but are stripped before building the Agent runtime request, whose +DTO only accepts ``name``/``comparison_operator``/``value``. + | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | comparison_operator | string,
**Available values:** "<", "=", ">", "after", "before", "contains", "empty", "end with", "in", "is", "is not", "not contains", "not empty", "not in", "start with", "≠", "≤", "≥" | *Enum:* `"<"`, `"="`, `">"`, `"after"`, `"before"`, `"contains"`, `"empty"`, `"end with"`, `"in"`, `"is"`, `"is not"`, `"not contains"`, `"not empty"`, `"not in"`, `"start with"`, `"≠"`, `"≤"`, `"≥"` | Yes | +| id | string | | No | +| metadata_id | string | | No | | name | string | | Yes | | value | string
[ string ]
number | | No | diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py index 36a4e9a5ee9..c969b296efb 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_runtime_request_builder.py @@ -744,7 +744,13 @@ def test_build_maps_agent_soul_knowledge_to_knowledge_layer_config(): "conditions": { "logical_operator": "and", "conditions": [ - {"name": "category", "comparison_operator": "contains", "value": "auth"} + { + "id": "cond-1", + "metadata_id": "meta-1", + "name": "category", + "comparison_operator": "contains", + "value": "auth", + } ], }, }, diff --git a/api/tests/unit_tests/services/agent/test_agent_composer_entities.py b/api/tests/unit_tests/services/agent/test_agent_composer_entities.py index 4aaae11b7dc..efc29ffb602 100644 --- a/api/tests/unit_tests/services/agent/test_agent_composer_entities.py +++ b/api/tests/unit_tests/services/agent/test_agent_composer_entities.py @@ -365,6 +365,51 @@ def test_knowledge_runtime_requirements_block_publish_but_not_draft_save(knowled ComposerConfigValidator.validate_publish_payload(publish_payload) +def test_manual_metadata_filtering_condition_accepts_composer_ui_identifiers(): + """The composer's condition editor always sends ``id`` (list row key) and + ``metadata_id`` (selected metadata field reference) alongside every + condition. Rejecting them as unknown fields broke every save of a manual + metadata filter (see GH issue #40169).""" + payload = ComposerSavePayload.model_validate( + { + "variant": ComposerVariant.AGENT_APP, + "save_strategy": ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION, + "agent_soul": { + "knowledge": { + "sets": [ + { + "id": "support", + "name": "Support KB", + "datasets": [{"id": "dataset-1"}], + "query": {"mode": "generated_query"}, + "retrieval": {"mode": "multiple", "top_k": 4}, + "metadata_filtering": { + "mode": "manual", + "conditions": { + "logical_operator": "and", + "conditions": [ + { + "id": "b149eceb-191a-40a2-9f11-61cf21ebd147", + "metadata_id": "ad6cf326-eadf-46e8-a2d5-9cb892d2cc84", + "name": "category", + "comparison_operator": "is", + "value": "auth", + } + ], + }, + }, + }, + ] + } + }, + } + ) + + condition = payload.agent_soul.knowledge.sets[0].metadata_filtering.conditions.conditions[0] + assert condition.id == "b149eceb-191a-40a2-9f11-61cf21ebd147" + assert condition.metadata_id == "ad6cf326-eadf-46e8-a2d5-9cb892d2cc84" + + def test_agent_soul_model_config_is_first_class_without_credentials(): config = AgentSoulConfig( model=AgentSoulModelConfig( diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index cfabaa87f32..fa33c2698b3 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -1882,6 +1882,8 @@ export type AgentKnowledgeMetadataCondition = { | '≠' | '≤' | '≥' + id?: string | null + metadata_id?: string | null name: string value?: string | Array | number | null } diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index 9ad871a9582..9b9c8878a2a 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -2357,6 +2357,14 @@ export const zAgentKnowledgeRetrievalConfig = z.object({ /** * AgentKnowledgeMetadataCondition + * + * One manual metadata filter clause. + * + * ``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on + * every save (a stable row key and a reference to the selected metadata + * field). They are persisted here for round-tripping the composer's draft + * state but are stripped before building the Agent runtime request, whose + * DTO only accepts ``name``/``comparison_operator``/``value``. */ export const zAgentKnowledgeMetadataCondition = z.object({ comparison_operator: z.enum([ @@ -2379,6 +2387,8 @@ export const zAgentKnowledgeMetadataCondition = z.object({ '≤', '≥', ]), + id: z.string().nullish(), + metadata_id: z.string().nullish(), name: z.string().min(1).max(255), value: z.union([z.string(), z.array(z.string()), z.number()]).nullish(), }) diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index ff09e5e48f0..1c5202c86ee 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -3097,6 +3097,8 @@ export type AgentKnowledgeMetadataCondition = { | '≠' | '≤' | '≥' + id?: string | null + metadata_id?: string | null name: string value?: string | Array | number | null } diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 0693ecd3dbe..41ce64d22be 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -4048,6 +4048,14 @@ export const zMessageInfiniteScrollPaginationResponse = z.object({ /** * AgentKnowledgeMetadataCondition + * + * One manual metadata filter clause. + * + * ``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on + * every save (a stable row key and a reference to the selected metadata + * field). They are persisted here for round-tripping the composer's draft + * state but are stripped before building the Agent runtime request, whose + * DTO only accepts ``name``/``comparison_operator``/``value``. */ export const zAgentKnowledgeMetadataCondition = z.object({ comparison_operator: z.enum([ @@ -4070,6 +4078,8 @@ export const zAgentKnowledgeMetadataCondition = z.object({ '≤', '≥', ]), + id: z.string().nullish(), + metadata_id: z.string().nullish(), name: z.string().min(1).max(255), value: z.union([z.string(), z.array(z.string()), z.number()]).nullish(), }) diff --git a/packages/contracts/generated/api/console/snippets/types.gen.ts b/packages/contracts/generated/api/console/snippets/types.gen.ts index d5b6a62cfc4..28b48d12ceb 100644 --- a/packages/contracts/generated/api/console/snippets/types.gen.ts +++ b/packages/contracts/generated/api/console/snippets/types.gen.ts @@ -1130,6 +1130,8 @@ export type AgentKnowledgeMetadataCondition = { | '≠' | '≤' | '≥' + id?: string | null + metadata_id?: string | null name: string value?: string | Array | number | null } diff --git a/packages/contracts/generated/api/console/snippets/zod.gen.ts b/packages/contracts/generated/api/console/snippets/zod.gen.ts index 4474c465df9..31c5ee2e3d1 100644 --- a/packages/contracts/generated/api/console/snippets/zod.gen.ts +++ b/packages/contracts/generated/api/console/snippets/zod.gen.ts @@ -1439,6 +1439,14 @@ export const zAgentKnowledgeRetrievalConfig = z.object({ /** * AgentKnowledgeMetadataCondition + * + * One manual metadata filter clause. + * + * ``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on + * every save (a stable row key and a reference to the selected metadata + * field). They are persisted here for round-tripping the composer's draft + * state but are stripped before building the Agent runtime request, whose + * DTO only accepts ``name``/``comparison_operator``/``value``. */ export const zAgentKnowledgeMetadataCondition = z.object({ comparison_operator: z.enum([ @@ -1461,6 +1469,8 @@ export const zAgentKnowledgeMetadataCondition = z.object({ '≤', '≥', ]), + id: z.string().nullish(), + metadata_id: z.string().nullish(), name: z.string().min(1).max(255), value: z.union([z.string(), z.array(z.string()), z.number()]).nullish(), })