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>
This commit is contained in:
chelsealong
2026-08-08 14:55:27 +08:00
committed by GitHub
parent d362bdbb4e
commit 2a742486c5
11 changed files with 115 additions and 2 deletions
@@ -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,
)
+11
View File
@@ -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
+10
View File
@@ -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, <br>**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<br>[ string ]<br>number | | No |
@@ -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",
}
],
},
},
@@ -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(
@@ -1882,6 +1882,8 @@ export type AgentKnowledgeMetadataCondition = {
| '≠'
| '≤'
| '≥'
id?: string | null
metadata_id?: string | null
name: string
value?: string | Array<string> | number | null
}
@@ -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(),
})
@@ -3097,6 +3097,8 @@ export type AgentKnowledgeMetadataCondition = {
| '≠'
| '≤'
| '≥'
id?: string | null
metadata_id?: string | null
name: string
value?: string | Array<string> | number | null
}
@@ -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(),
})
@@ -1130,6 +1130,8 @@ export type AgentKnowledgeMetadataCondition = {
| '≠'
| '≤'
| '≥'
id?: string | null
metadata_id?: string | null
name: string
value?: string | Array<string> | number | null
}
@@ -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(),
})