mirror of
https://github.com/langgenius/dify.git
synced 2026-09-01 15:09:21 +08:00
fix(agent): clean legacy Agent Soul files (#41024)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
"""clean legacy agent soul files
|
||||
|
||||
Revision ID: fbdfcf5f5a6e
|
||||
Revises: 89919253ca7a
|
||||
Create Date: 2026-08-20 09:38:36.827807
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "fbdfcf5f5a6e"
|
||||
down_revision = "89919253ca7a"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _remove_legacy_files(table_name: str) -> None:
|
||||
dialect_name = op.get_context().dialect.name
|
||||
if dialect_name == "postgresql":
|
||||
op.execute(
|
||||
f"""UPDATE {table_name}
|
||||
SET config_snapshot = (config_snapshot::jsonb - 'files')::text
|
||||
WHERE config_snapshot::jsonb ? 'files'"""
|
||||
)
|
||||
return
|
||||
if dialect_name == "mysql":
|
||||
op.execute(
|
||||
f"""UPDATE {table_name}
|
||||
SET config_snapshot = JSON_REMOVE(config_snapshot, '$.files')
|
||||
WHERE JSON_CONTAINS_PATH(config_snapshot, 'one', '$.files')"""
|
||||
)
|
||||
return
|
||||
if dialect_name == "sqlite":
|
||||
op.execute(
|
||||
f"""UPDATE {table_name}
|
||||
SET config_snapshot = json_remove(config_snapshot, '$.files')
|
||||
WHERE json_type(config_snapshot, '$.files') IS NOT NULL"""
|
||||
)
|
||||
return
|
||||
raise RuntimeError(f"unsupported database dialect: {dialect_name}")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# The Agent Drive removal migration skipped its Python row rewrite when
|
||||
# migrations were emitted as offline SQL. Run the cleanup again as native
|
||||
# SQL so every deployment removes the retired AgentSoulConfig.files field.
|
||||
_remove_legacy_files("agent_config_snapshots")
|
||||
_remove_legacy_files("agent_config_drafts")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# The retired files catalog cannot be reconstructed after Agent Drive data
|
||||
# has been removed.
|
||||
pass
|
||||
@@ -0,0 +1,125 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
from pydantic import ValidationError
|
||||
|
||||
from models.agent_config_entities import AgentSoulConfig
|
||||
|
||||
_MIGRATION_PATH = (
|
||||
Path(__file__).resolve().parents[3]
|
||||
/ "migrations/versions/2026_08_20_0938-fbdfcf5f5a6e_clean_legacy_agent_soul_files.py"
|
||||
)
|
||||
|
||||
|
||||
def _load_migration_module() -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location("clean_legacy_agent_soul_files", _MIGRATION_PATH)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError("failed to load migration module")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _run_upgrade(module: ModuleType, engine: sa.Engine) -> None:
|
||||
with engine.begin() as connection:
|
||||
operations = Operations(MigrationContext.configure(connection))
|
||||
original_op = module.__dict__["op"]
|
||||
module.__dict__["op"] = operations
|
||||
try:
|
||||
module.__dict__["upgrade"]()
|
||||
finally:
|
||||
module.__dict__["op"] = original_op
|
||||
|
||||
|
||||
def test_upgrade_makes_legacy_build_draft_valid_for_first_message() -> None:
|
||||
engine = sa.create_engine("sqlite:///:memory:")
|
||||
metadata = sa.MetaData()
|
||||
for table_name in ("agent_config_snapshots", "agent_config_drafts"):
|
||||
sa.Table(
|
||||
table_name,
|
||||
metadata,
|
||||
sa.Column("id", sa.String(36), primary_key=True),
|
||||
sa.Column("config_snapshot", sa.Text(), nullable=False),
|
||||
)
|
||||
metadata.create_all(engine)
|
||||
|
||||
legacy_soul: dict[str, object] = {
|
||||
"files": {"files": [], "skills": []},
|
||||
"prompt": {"system_prompt": "Build mode"},
|
||||
}
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
AgentSoulConfig.model_validate(legacy_soul)
|
||||
assert exc_info.value.errors(include_url=False)[0]["loc"] == ("files",)
|
||||
assert exc_info.value.errors(include_url=False)[0]["type"] == "extra_forbidden"
|
||||
|
||||
with engine.begin() as connection:
|
||||
for table_name in ("agent_config_snapshots", "agent_config_drafts"):
|
||||
connection.execute(
|
||||
sa.text(f"INSERT INTO {table_name} (id, config_snapshot) VALUES (:id, :config_snapshot)"),
|
||||
{"id": table_name, "config_snapshot": json.dumps(legacy_soul)},
|
||||
)
|
||||
|
||||
_run_upgrade(_load_migration_module(), engine)
|
||||
|
||||
with engine.begin() as connection:
|
||||
stored_build_draft = connection.execute(sa.text("SELECT config_snapshot FROM agent_config_drafts")).scalar_one()
|
||||
stored_snapshot = connection.execute(sa.text("SELECT config_snapshot FROM agent_config_snapshots")).scalar_one()
|
||||
|
||||
for stored_soul in (stored_build_draft, stored_snapshot):
|
||||
value = json.loads(stored_soul)
|
||||
assert "files" not in value
|
||||
assert AgentSoulConfig.model_validate(value).prompt.system_prompt == "Build mode"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("dialect_name", "removal_expression", "presence_predicate", "other_dialect_expression"),
|
||||
[
|
||||
(
|
||||
"postgresql",
|
||||
"config_snapshot::jsonb - 'files'",
|
||||
"config_snapshot::jsonb ? 'files'",
|
||||
"JSON_REMOVE",
|
||||
),
|
||||
(
|
||||
"mysql",
|
||||
"JSON_REMOVE(config_snapshot, '$.files')",
|
||||
"JSON_CONTAINS_PATH(config_snapshot, 'one', '$.files')",
|
||||
"config_snapshot::jsonb",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_upgrade_emits_legacy_files_cleanup_in_offline_sql(
|
||||
dialect_name: str,
|
||||
removal_expression: str,
|
||||
presence_predicate: str,
|
||||
other_dialect_expression: str,
|
||||
) -> None:
|
||||
module = _load_migration_module()
|
||||
output = StringIO()
|
||||
migration_context = MigrationContext.configure(
|
||||
dialect_name=dialect_name,
|
||||
opts={"as_sql": True, "output_buffer": output},
|
||||
)
|
||||
operations = Operations(migration_context)
|
||||
original_op = module.__dict__["op"]
|
||||
module.__dict__["op"] = operations
|
||||
try:
|
||||
module.__dict__["upgrade"]()
|
||||
finally:
|
||||
module.__dict__["op"] = original_op
|
||||
|
||||
generated_sql = output.getvalue()
|
||||
assert "UPDATE agent_config_snapshots" in generated_sql
|
||||
assert "UPDATE agent_config_drafts" in generated_sql
|
||||
assert generated_sql.count(removal_expression) == 2
|
||||
assert generated_sql.count(presence_predicate) == 2
|
||||
assert other_dialect_expression not in generated_sql
|
||||
Reference in New Issue
Block a user