Skip to content

Commit 34eaf3a

Browse files
committed
fix(agent-v2): simplify agent app config flow
1 parent f5cd587 commit 34eaf3a

7 files changed

Lines changed: 21 additions & 109 deletions

File tree

api/core/app/apps/agent_app/app_generator.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,13 @@
99
from __future__ import annotations
1010

1111
import contextvars
12-
import json
1312
import logging
1413
import threading
1514
import uuid
16-
from collections.abc import Generator, Mapping, Sequence
15+
from collections.abc import Generator, Mapping
1716
from typing import Any, Literal
1817

1918
from flask import Flask, current_app
20-
from pydantic import JsonValue
2119
from sqlalchemy import and_, or_, select
2220

2321
from clients.agent_backend import AgentBackendRunEventAdapter
@@ -63,13 +61,6 @@ class AgentAppGeneratorError(ValueError):
6361
"""Raised when an Agent App turn cannot be set up."""
6462

6563

66-
def _append_prompt_file_mappings(query: str, prompt_file_mappings: Sequence[JsonValue]) -> str:
67-
"""Append raw request file references to the backend user prompt."""
68-
if not prompt_file_mappings:
69-
return query
70-
return f"{query}\n{json.dumps(list(prompt_file_mappings), ensure_ascii=False)}"
71-
72-
7364
class AgentAppGenerator(MessageBasedAppGenerator):
7465
def generate(
7566
self,
@@ -88,7 +79,6 @@ def generate(
8879
raise AgentAppGeneratorError("query is required")
8980
query = query.replace("\x00", "")
9081
inputs = args["inputs"]
91-
prompt_file_mappings = args.get("files") or []
9282

9383
# Resolve the bound roster Agent + its current Agent Soul snapshot.
9484
agent, agent_config_id, agent_config_version_kind, agent_soul = self._resolve_agent(
@@ -132,7 +122,6 @@ def generate(
132122
),
133123
query=query,
134124
files=[],
135-
prompt_file_mappings=prompt_file_mappings,
136125
parent_message_id=(
137126
args.get("parent_message_id")
138127
if invoke_from not in {InvokeFrom.SERVICE_API, InvokeFrom.OPENAPI}
@@ -366,10 +355,6 @@ def _generate_worker(
366355
)
367356
if handled:
368357
return
369-
query = _append_prompt_file_mappings(
370-
query=query,
371-
prompt_file_mappings=application_generate_entity.prompt_file_mappings,
372-
)
373358

374359
dify_context = DifyRunContext(
375360
tenant_id=app_config.tenant_id,

api/core/app/entities/app_invoke_entities.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from enum import StrEnum
33
from typing import TYPE_CHECKING, Any, Literal
44

5-
from pydantic import BaseModel, ConfigDict, Field, JsonValue, ValidationInfo, field_validator
5+
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator
66

77
from constants import UUID_NIL
88
from core.app.app_config.entities import EasyUIBasedAppConfig, WorkflowUIBasedAppConfig
@@ -227,17 +227,12 @@ class AgentAppGenerateEntity(ChatAppGenerateEntity):
227227
228228
``agent_runtime_session_snapshot_id`` carries the runtime session scope
229229
used to resume or suspend within the same editable config surface.
230-
231-
``prompt_file_mappings`` preserves the raw request ``files`` array for the
232-
Agent backend prompt. These references are appended to the backend prompt
233-
text while the stored chat message keeps the user's original query.
234230
"""
235231

236232
agent_id: str
237233
agent_config_snapshot_id: str
238234
agent_config_version_kind: Literal["snapshot", "draft", "build_draft"] = "snapshot"
239235
agent_runtime_session_snapshot_id: str | None = None
240-
prompt_file_mappings: Sequence[JsonValue] = Field(default_factory=list)
241236

242237

243238
class AdvancedChatAppGenerateEntity(ConversationAppGenerateEntity):

api/services/agent_config_service.py

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,15 @@
1818
import zipfile
1919
from dataclasses import dataclass
2020
from enum import StrEnum
21-
from operator import itemgetter
2221
from typing import Literal
2322

23+
from core.app.file_access.controller import DatabaseFileAccessController
2424
from dotenv import dotenv_values
2525
from pydantic import BaseModel, ConfigDict, Field
2626
from sqlalchemy import select
2727
from sqlalchemy.exc import DataError, SQLAlchemyError
2828
from sqlalchemy.orm import Session
2929

30-
from core.app.file_access.controller import DatabaseFileAccessController
3130
from core.db.session_factory import session_factory
3231
from core.tools.tool_file_manager import ToolFileManager
3332
from extensions.ext_storage import storage
@@ -662,9 +661,7 @@ def preview_file(
662661
file_kind=file_ref.file_kind,
663662
file_id=file_ref.file_id,
664663
)
665-
return self._preview_bytes(
666-
path=filename or file_ref.name, size=file_ref.size, payload=payload, field_name="name"
667-
)
664+
return self._preview_bytes(path=filename or file_ref.name, size=file_ref.size, payload=payload, field_name="name")
668665

669666
def update_env(
670667
self,
@@ -1120,14 +1117,8 @@ def _manifest_for_target(target: AgentConfigTarget) -> dict[str, object]:
11201117
return {
11211118
"agent_id": target.agent_id,
11221119
"config_version": AgentConfigService._config_version_payload(target),
1123-
"skills": {
1124-
"items": [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]
1125-
},
1126-
"files": {
1127-
"items": [
1128-
AgentConfigService._serialize_file_item(file_ref) for file_ref in target.agent_soul.config_files
1129-
]
1130-
},
1120+
"skills": {"items": [AgentConfigService._serialize_skill_item(skill) for skill in target.agent_soul.config_skills]},
1121+
"files": {"items": [AgentConfigService._serialize_file_item(file_ref) for file_ref in target.agent_soul.config_files]},
11311122
"env_keys": AgentConfigService._env_keys(target.agent_soul),
11321123
"note": target.agent_soul.config_note,
11331124
}
@@ -1196,18 +1187,12 @@ def _preview_bytes(
11961187
def _normalize_archive_member_path(cls, path: str) -> str:
11971188
normalized = path.replace("\\", "/").strip("/")
11981189
if not normalized:
1199-
raise AgentConfigServiceError(
1200-
"config_skill_file_invalid", "config skill file path is invalid", status_code=400
1201-
)
1190+
raise AgentConfigServiceError("config_skill_file_invalid", "config skill file path is invalid", status_code=400)
12021191
if "\x00" in normalized or any(ord(ch) < 0x20 for ch in normalized):
1203-
raise AgentConfigServiceError(
1204-
"config_skill_file_invalid", "config skill file path is invalid", status_code=400
1205-
)
1192+
raise AgentConfigServiceError("config_skill_file_invalid", "config skill file path is invalid", status_code=400)
12061193
segments = normalized.split("/")
12071194
if any(not segment or segment in {".", ".."} for segment in segments):
1208-
raise AgentConfigServiceError(
1209-
"config_skill_file_invalid", "config skill file path is invalid", status_code=400
1210-
)
1195+
raise AgentConfigServiceError("config_skill_file_invalid", "config skill file path is invalid", status_code=400)
12111196
return "/".join(segments)
12121197

12131198
@classmethod
@@ -1219,7 +1204,8 @@ def _inspect_skill_archive(cls, archive_bytes: bytes) -> tuple[list[dict[str, ob
12191204
for info in archive.infolist():
12201205
normalized_path = cls._normalize_archive_member_path(info.filename)
12211206
segments = normalized_path.split("/")
1222-
directories.update("/".join(segments[:index]) for index in range(1, len(segments)))
1207+
for index in range(1, len(segments)):
1208+
directories.add("/".join(segments[:index]))
12231209
if info.is_dir():
12241210
directories.add(normalized_path)
12251211
continue
@@ -1238,7 +1224,7 @@ def _inspect_skill_archive(cls, archive_bytes: bytes) -> tuple[list[dict[str, ob
12381224
if skill_md_preview is None or skill_md_preview["binary"]:
12391225
raise ValueError("skill archive is missing a text SKILL.md")
12401226

1241-
directory_items: list[dict[str, object]] = [
1227+
directory_items = [
12421228
{
12431229
"path": path,
12441230
"name": path.rsplit("/", 1)[-1],
@@ -1248,7 +1234,7 @@ def _inspect_skill_archive(cls, archive_bytes: bytes) -> tuple[list[dict[str, ob
12481234
}
12491235
for path in sorted(directories)
12501236
]
1251-
files = sorted([*directory_items, *members], key=itemgetter("path", "type"))
1237+
files = sorted([*directory_items, *members], key=lambda item: (item["path"], item["type"]))
12521238
return files, skill_md_preview
12531239

12541240
def _load_skill_archive_member(self, *, tenant_id: str, file_id: str, path: str) -> bytes:
@@ -1323,9 +1309,7 @@ def _load_file_ref_bytes(
13231309
return storage.load_once(upload_file.key), upload_file.name, upload_file.mime_type
13241310

13251311
@staticmethod
1326-
def _resolve_download_url(
1327-
*, tenant_id: str, file_kind: Literal["upload_file", "tool_file"], file_id: str
1328-
) -> str | None:
1312+
def _resolve_download_url(*, tenant_id: str, file_kind: Literal["upload_file", "tool_file"], file_id: str) -> str | None:
13291313
controller = DatabaseFileAccessController()
13301314
from core.app.workflow.file_runtime import DifyWorkflowFileRuntime
13311315

api/tasks/app_generate/resume_agent_app_task.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def resume_agent_app_execution(*, conversation_id: str, form_id: str) -> None:
5151
app_model=app_model,
5252
user=user,
5353
conversation_id=conversation_id,
54-
invoke_from=_resolve_invoke_from(conversation),
54+
invoke_from=conversation.invoke_from or InvokeFrom.WEB_APP,
5555
)
5656
except Exception:
5757
logger.exception("Agent App resume failed for conversation %s form %s", conversation_id, form_id)
@@ -68,9 +68,3 @@ def _resolve_conversation_user(*, app_model: App, conversation: Conversation) ->
6868
if conversation.from_end_user_id:
6969
return db.session.get(EndUser, conversation.from_end_user_id)
7070
return None
71-
72-
73-
def _resolve_invoke_from(conversation: Conversation) -> InvokeFrom:
74-
if conversation.invoke_from is None:
75-
return InvokeFrom.WEB_APP
76-
return InvokeFrom.value_of(conversation.invoke_from.value)

api/tests/unit_tests/core/app/apps/agent_app/test_app_generator.py

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from __future__ import annotations
1111

1212
import contextlib
13-
import json
1413

1514
import pytest
1615
from pytest_mock import MockerFixture
@@ -98,26 +97,16 @@ def test_generate_orchestrates_and_starts_worker(self, generator, mocker: Mocker
9897
)
9998
mocker.patch(f"{MODULE}.ModelConfigConverter.convert", return_value=mocker.MagicMock(model="gpt-4o-mini"))
10099
mocker.patch(f"{MODULE}.TraceQueueManager", return_value=mocker.MagicMock())
101-
generate_entity = mocker.patch(
102-
f"{MODULE}.AgentAppGenerateEntity", return_value=mocker.MagicMock(task_id="t", user_id="user")
103-
)
100+
mocker.patch(f"{MODULE}.AgentAppGenerateEntity", return_value=mocker.MagicMock(task_id="t", user_id="user"))
104101
mocker.patch(f"{MODULE}.MessageBasedAppQueueManager", return_value=mocker.MagicMock())
105102
thread_obj = mocker.MagicMock()
106103
mocker.patch(f"{MODULE}.threading.Thread", return_value=thread_obj)
107104
mocker.patch(f"{MODULE}.AgentAppGenerateResponseConverter.convert", return_value={"result": "ok"})
108-
file_mappings = [
109-
{
110-
"type": "image",
111-
"transfer_method": "local_file",
112-
"url": "",
113-
"upload_file_id": "upload-file-1",
114-
}
115-
]
116105

117106
result = generator.generate(
118107
app_model=app_model,
119108
user=user,
120-
args={"query": "hello", "inputs": {"name": "world"}, "files": file_mappings},
109+
args={"query": "hello", "inputs": {"name": "world"}},
121110
invoke_from=InvokeFrom.WEB_APP,
122111
streaming=True,
123112
)
@@ -130,7 +119,6 @@ def test_generate_orchestrates_and_starts_worker(self, generator, mocker: Mocker
130119
draft_type=None,
131120
user=user,
132121
)
133-
assert generate_entity.call_args.kwargs["prompt_file_mappings"] == file_mappings
134122

135123
def test_generate_loads_existing_conversation(self, generator: AgentAppGenerator, mocker: MockerFixture):
136124
app_model = mocker.MagicMock(id="app1", tenant_id="tenant", mode="agent")
@@ -211,18 +199,10 @@ def ctx_manager(*args, **kwargs):
211199

212200
mocker.patch("libs.flask_utils.preserve_flask_contexts", ctx_manager)
213201

214-
def _wire(
215-
self,
216-
generator: AgentAppGenerator,
217-
mocker: MockerFixture,
218-
*,
219-
run_side_effect=None,
220-
handled=False,
221-
guard_query="query",
222-
):
202+
def _wire(self, generator: AgentAppGenerator, mocker: MockerFixture, *, run_side_effect=None, handled=False):
223203
generator._get_conversation = mocker.MagicMock(return_value=mocker.MagicMock(id="conv"))
224204
generator._get_message = mocker.MagicMock(return_value=mocker.MagicMock(id="msg"))
225-
generator._run_input_guards = mocker.MagicMock(return_value=(handled, guard_query))
205+
generator._run_input_guards = mocker.MagicMock(return_value=(handled, "query"))
226206
generator._resolve_agent_by_id = mocker.MagicMock(
227207
return_value=(mocker.MagicMock(), mocker.MagicMock(), mocker.MagicMock())
228208
)
@@ -249,7 +229,6 @@ def _call(
249229
is_resume=False,
250230
query="query",
251231
runtime_session_snapshot_id="s",
252-
prompt_file_mappings=(),
253232
):
254233
generator._generate_worker(
255234
flask_app=mocker.MagicMock(),
@@ -260,7 +239,6 @@ def _call(
260239
agent_runtime_session_snapshot_id=runtime_session_snapshot_id,
261240
model_conf=mocker.MagicMock(model="m"),
262241
query=query,
263-
prompt_file_mappings=prompt_file_mappings,
264242
),
265243
queue_manager=queue_manager,
266244
conversation_id="conv",
@@ -285,30 +263,6 @@ def test_worker_passes_runtime_session_scope_to_runner(self, generator, mocker:
285263
assert runner.run.call_args.kwargs["agent_config_snapshot_id"] == "s"
286264
assert runner.run.call_args.kwargs["session_scope_snapshot_id"] is None
287265

288-
def test_worker_appends_prompt_files_to_backend_query(self, generator, mocker: MockerFixture):
289-
runner = self._wire(generator, mocker, guard_query="你看得见这张图片吗")
290-
queue_manager = mocker.MagicMock()
291-
file_mappings = [
292-
{
293-
"type": "image",
294-
"transfer_method": "local_file",
295-
"url": "",
296-
"upload_file_id": "upload-file-1",
297-
}
298-
]
299-
300-
self._call(
301-
generator,
302-
mocker,
303-
queue_manager,
304-
query="你看得见这张图片吗",
305-
prompt_file_mappings=file_mappings,
306-
)
307-
308-
assert runner.run.call_args.kwargs["query"] == (
309-
f"你看得见这张图片吗\n{json.dumps(file_mappings, ensure_ascii=False)}"
310-
)
311-
312266
def test_input_guard_short_circuit_skips_backend(self, generator, mocker: MockerFixture):
313267
runner = self._wire(generator, mocker, handled=True)
314268
queue_manager = mocker.MagicMock()

web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { AgentConfigApiContext } from '../../config-context'
21
import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
32
import { toast } from '@langgenius/dify-ui/toast'
43
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
@@ -10,6 +9,7 @@ import { formStateToAgentSoulConfig } from '@/features/agent-v2/agent-composer/c
109
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
1110
import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider'
1211
import { agentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store'
12+
import type { AgentConfigApiContext } from '../../config-context'
1313
import { AgentConfigApiContextProvider } from '../../config-context'
1414
import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
1515
import { AgentFiles } from '../index'

web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { AgentConfigApiContext } from '../../config-context'
21
import { toast } from '@langgenius/dify-ui/toast'
32
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
43
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
@@ -9,6 +8,7 @@ import { formStateToAgentSoulConfig } from '@/features/agent-v2/agent-composer/c
98
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
109
import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider'
1110
import { agentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store'
11+
import type { AgentConfigApiContext } from '../../config-context'
1212
import { AgentConfigApiContextProvider } from '../../config-context'
1313
import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
1414
import { AgentSkills } from '../index'

0 commit comments

Comments
 (0)