feat(ai): add tenant-safe hierarchical expense learning

This commit is contained in:
caoxiaozhu
2026-07-16 14:30:41 +08:00
parent 6bdf65bc24
commit ee88a36baf
65 changed files with 6909 additions and 232 deletions

View File

@@ -0,0 +1,133 @@
"""add hierarchical expense memory governance fields
Revision ID: 20260716_0007
Revises: 20260716_0006
Create Date: 2026-07-16 12:05:00
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260716_0007"
down_revision: str | None = "20260716_0006"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.add_column(
"memory_entries",
sa.Column(
"origin_type",
sa.String(length=24),
server_default="learned",
nullable=False,
),
)
op.add_column(
"memory_entries",
sa.Column("managed_by", sa.String(length=255), nullable=True),
)
op.add_column(
"memory_entries",
sa.Column("managed_at", sa.DateTime(timezone=True), nullable=True),
)
op.add_column(
"memory_entries",
sa.Column("management_reason", sa.String(length=255), nullable=True),
)
op.add_column(
"memory_entries",
sa.Column("policy_version", sa.String(length=64), nullable=True),
)
op.drop_constraint(
"ck_memory_entries_scope_type",
"memory_entries",
type_="check",
)
op.create_check_constraint(
"ck_memory_entries_scope_type",
"memory_entries",
"scope_type IN ('user', 'department', 'enterprise')",
)
op.create_check_constraint(
"ck_memory_entries_origin_type",
"memory_entries",
"origin_type IN ('learned', 'admin_managed')",
)
op.create_check_constraint(
"ck_memory_entries_scope_origin",
"memory_entries",
"(scope_type = 'user' AND origin_type = 'learned') OR "
"(scope_type IN ('department', 'enterprise') "
"AND origin_type = 'admin_managed')",
)
op.create_check_constraint(
"ck_memory_entries_enterprise_scope",
"memory_entries",
"scope_type != 'enterprise' OR scope_id = tenant_id",
)
op.create_check_constraint(
"ck_memory_entries_management_audit",
"memory_entries",
"(origin_type = 'learned' AND managed_by IS NULL "
"AND managed_at IS NULL AND management_reason IS NULL) OR "
"(origin_type = 'admin_managed' AND managed_by IS NOT NULL "
"AND length(trim(managed_by)) > 0 AND managed_at IS NOT NULL "
"AND management_reason IS NOT NULL "
"AND length(trim(management_reason)) > 0 "
"AND policy_version IS NOT NULL AND length(trim(policy_version)) > 0)",
)
def downgrade() -> None:
op.drop_constraint(
"ck_memory_entries_management_audit",
"memory_entries",
type_="check",
)
op.drop_constraint(
"ck_memory_entries_enterprise_scope",
"memory_entries",
type_="check",
)
op.drop_constraint(
"ck_memory_entries_scope_origin",
"memory_entries",
type_="check",
)
op.drop_constraint(
"ck_memory_entries_origin_type",
"memory_entries",
type_="check",
)
op.drop_constraint(
"ck_memory_entries_scope_type",
"memory_entries",
type_="check",
)
# 0006 只认识个人记忆。组织记忆无法无损映射回旧结构,因此降级时
# 先解除组织版本间的自引用并删除组织作用域记录,个人记忆完整保留。
op.execute(
"UPDATE memory_entries SET superseded_by_id = NULL "
"WHERE scope_type IN ('department', 'enterprise')"
)
op.execute(
"DELETE FROM memory_entries "
"WHERE scope_type IN ('department', 'enterprise')"
)
op.create_check_constraint(
"ck_memory_entries_scope_type",
"memory_entries",
"scope_type = 'user'",
)
op.drop_column("memory_entries", "policy_version")
op.drop_column("memory_entries", "management_reason")
op.drop_column("memory_entries", "managed_at")
op.drop_column("memory_entries", "managed_by")
op.drop_column("memory_entries", "origin_type")