feat: 重构 memory 系统,支持 user memory 和 work memory 分离
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
"""drop source column from memories
|
||||
|
||||
Revision ID: 202603230001
|
||||
Revises: 202603200001
|
||||
Create Date: 2026-03-23 18:00:00
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "202603230001"
|
||||
down_revision: Union[str, Sequence[str], None] = "202603200001"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
columns = {column["name"] for column in inspector.get_columns("memories")}
|
||||
if "source" in columns:
|
||||
op.drop_column("memories", "source")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
columns = {column["name"] for column in inspector.get_columns("memories")}
|
||||
if "source" not in columns:
|
||||
op.add_column(
|
||||
"memories",
|
||||
sa.Column(
|
||||
"source", sa.String(length=20), nullable=False, server_default="agent"
|
||||
),
|
||||
)
|
||||
op.alter_column("memories", "source", server_default=None)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""drop title column from memories
|
||||
|
||||
Revision ID: 202603230002
|
||||
Revises: 202603230001
|
||||
Create Date: 2026-03-23 21:00:00
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "202603230002"
|
||||
down_revision: Union[str, Sequence[str], None] = "202603230001"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
columns = {column["name"] for column in inspector.get_columns("memories")}
|
||||
if "title" in columns:
|
||||
op.drop_column("memories", "title")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
columns = {column["name"] for column in inspector.get_columns("memories")}
|
||||
if "title" not in columns:
|
||||
op.add_column(
|
||||
"memories", sa.Column("title", sa.String(length=255), nullable=True)
|
||||
)
|
||||
@@ -0,0 +1,355 @@
|
||||
"""add bootstrap key and unique indexes for registration bootstrap
|
||||
|
||||
Revision ID: 202603230003
|
||||
Revises: 202603230002
|
||||
Create Date: 2026-03-23 23:10:00
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "202603230003"
|
||||
down_revision: Union[str, Sequence[str], None] = "202603230002"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
automation_columns = {
|
||||
column["name"] for column in inspector.get_columns("automation_jobs")
|
||||
}
|
||||
if "bootstrap_key" not in automation_columns:
|
||||
op.add_column(
|
||||
"automation_jobs",
|
||||
sa.Column("bootstrap_key", sa.String(length=64), nullable=True),
|
||||
)
|
||||
|
||||
op.execute("DROP INDEX IF EXISTS ux_automation_jobs_owner_memory_active")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE public.automation_jobs
|
||||
SET bootstrap_key = 'memory_extraction'
|
||||
WHERE bootstrap_key IS NULL
|
||||
AND (
|
||||
config->>'agent_type' = 'memory'
|
||||
OR (
|
||||
created_by = owner_id
|
||||
AND title = 'Memory Agent'
|
||||
AND coalesce(config->'enabled_tools', '[]'::jsonb) @> '["memory.write", "memory.forget"]'::jsonb
|
||||
AND jsonb_array_length(coalesce(config->'enabled_tools', '[]'::jsonb)) = 2
|
||||
AND coalesce(config->'context', '{}'::jsonb) @> jsonb_build_object(
|
||||
'source', 'latest_chat',
|
||||
'window_mode', 'day',
|
||||
'window_count', 2
|
||||
)
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS public.memories_dedup_backup_202603230003
|
||||
(LIKE public.memories INCLUDING ALL)
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS public.automation_jobs_dedup_backup_202603230003 (
|
||||
id UUID PRIMARY KEY
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
id,
|
||||
row_number() OVER (
|
||||
PARTITION BY owner_id, bootstrap_key
|
||||
ORDER BY updated_at DESC, created_at DESC, id DESC
|
||||
) AS rn
|
||||
FROM public.automation_jobs
|
||||
WHERE deleted_at IS NULL
|
||||
AND bootstrap_key IS NOT NULL
|
||||
)
|
||||
INSERT INTO public.automation_jobs_dedup_backup_202603230003(id)
|
||||
SELECT id
|
||||
FROM ranked
|
||||
WHERE rn > 1
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
id,
|
||||
row_number() OVER (
|
||||
PARTITION BY owner_id, memory_type
|
||||
ORDER BY updated_at DESC, created_at DESC, id DESC
|
||||
) AS rn
|
||||
FROM public.memories
|
||||
)
|
||||
INSERT INTO public.memories_dedup_backup_202603230003
|
||||
SELECT m.*
|
||||
FROM public.memories m
|
||||
JOIN ranked r ON r.id = m.id
|
||||
WHERE r.rn > 1
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
id,
|
||||
row_number() OVER (
|
||||
PARTITION BY owner_id, bootstrap_key
|
||||
ORDER BY updated_at DESC, created_at DESC, id DESC
|
||||
) AS rn
|
||||
FROM public.automation_jobs
|
||||
WHERE deleted_at IS NULL
|
||||
AND bootstrap_key IS NOT NULL
|
||||
)
|
||||
UPDATE public.automation_jobs aj
|
||||
SET deleted_at = now()
|
||||
FROM ranked r
|
||||
WHERE aj.id = r.id
|
||||
AND r.rn > 1
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_automation_jobs_owner_bootstrap_key_active
|
||||
ON public.automation_jobs(owner_id, bootstrap_key)
|
||||
WHERE deleted_at IS NULL
|
||||
AND bootstrap_key IS NOT NULL
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
id,
|
||||
row_number() OVER (
|
||||
PARTITION BY owner_id, memory_type
|
||||
ORDER BY updated_at DESC, created_at DESC, id DESC
|
||||
) AS rn
|
||||
FROM public.memories
|
||||
)
|
||||
DELETE FROM public.memories m
|
||||
USING ranked r
|
||||
WHERE m.id = r.id
|
||||
AND r.rn > 1
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_memories_owner_memory_type
|
||||
ON public.memories(owner_id, memory_type)
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
base_seed TEXT;
|
||||
candidate_username TEXT;
|
||||
attempt INT := 0;
|
||||
BEGIN
|
||||
base_seed := coalesce(NEW.phone, NEW.id::text);
|
||||
|
||||
LOOP
|
||||
candidate_username := 'user_' || public.generate_profile_username_suffix(base_seed || ':' || attempt::text);
|
||||
EXIT WHEN NOT EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.username = candidate_username
|
||||
);
|
||||
attempt := attempt + 1;
|
||||
IF attempt >= 50 THEN
|
||||
candidate_username := 'user_' || substr(replace(NEW.id::text, '-', ''), 1, 6);
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
candidate_username,
|
||||
NULL,
|
||||
NULL,
|
||||
'{}'::jsonb,
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP INDEX IF EXISTS ux_memories_owner_memory_type")
|
||||
op.execute("DROP INDEX IF EXISTS ux_automation_jobs_owner_bootstrap_key_active")
|
||||
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
tables = set(inspector.get_table_names(schema="public"))
|
||||
|
||||
if "automation_jobs_dedup_backup_202603230003" in tables:
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE public.automation_jobs aj
|
||||
SET deleted_at = NULL
|
||||
FROM public.automation_jobs_dedup_backup_202603230003 b
|
||||
WHERE aj.id = b.id
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"DROP TABLE IF EXISTS public.automation_jobs_dedup_backup_202603230003"
|
||||
)
|
||||
|
||||
if "memories_dedup_backup_202603230003" in tables:
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO public.memories
|
||||
SELECT b.*
|
||||
FROM public.memories_dedup_backup_202603230003 b
|
||||
LEFT JOIN public.memories m ON m.id = b.id
|
||||
WHERE m.id IS NULL
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
"""
|
||||
)
|
||||
op.execute("DROP TABLE IF EXISTS public.memories_dedup_backup_202603230003")
|
||||
|
||||
automation_columns = {
|
||||
column["name"] for column in inspector.get_columns("automation_jobs")
|
||||
}
|
||||
if "bootstrap_key" in automation_columns:
|
||||
op.drop_column("automation_jobs", "bootstrap_key")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_automation_jobs_owner_memory_active
|
||||
ON public.automation_jobs(owner_id)
|
||||
WHERE deleted_at IS NULL
|
||||
AND config->>'agent_type' = 'memory'
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
base_seed TEXT;
|
||||
candidate_username TEXT;
|
||||
attempt INT := 0;
|
||||
BEGIN
|
||||
base_seed := coalesce(NEW.phone, NEW.id::text);
|
||||
|
||||
LOOP
|
||||
candidate_username := 'user_' || public.generate_profile_username_suffix(base_seed || ':' || attempt::text);
|
||||
EXIT WHEN NOT EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.username = candidate_username
|
||||
);
|
||||
attempt := attempt + 1;
|
||||
IF attempt >= 50 THEN
|
||||
candidate_username := 'user_' || substr(replace(NEW.id::text, '-', ''), 1, 6);
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
candidate_username,
|
||||
NULL,
|
||||
NULL,
|
||||
'{}'::jsonb,
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.automation_jobs aj
|
||||
WHERE aj.owner_id = NEW.id
|
||||
AND aj.deleted_at IS NULL
|
||||
AND aj.config->>'agent_type' = 'memory'
|
||||
) THEN
|
||||
INSERT INTO public.automation_jobs (
|
||||
id,
|
||||
owner_id,
|
||||
title,
|
||||
config,
|
||||
schedule_type,
|
||||
run_at,
|
||||
next_run_at,
|
||||
timezone,
|
||||
status,
|
||||
created_by,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
NEW.id,
|
||||
'Memory Agent',
|
||||
jsonb_build_object(
|
||||
'agent_type', 'memory',
|
||||
'model_code', 'qwen3.5-flash',
|
||||
'enabled_tools', jsonb_build_array('calendar.read', 'user.lookup'),
|
||||
'input_template', '请基于最近聊天上下文生成一段可执行的记忆总结与建议。',
|
||||
'context', jsonb_build_object(
|
||||
'source', 'latest_chat',
|
||||
'window_mode', 'day',
|
||||
'window_count', 2
|
||||
)
|
||||
),
|
||||
'daily',
|
||||
now(),
|
||||
now() + interval '1 day',
|
||||
'UTC',
|
||||
'active',
|
||||
NEW.id,
|
||||
now(),
|
||||
now()
|
||||
);
|
||||
END IF;
|
||||
EXCEPTION WHEN unique_violation THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
"""
|
||||
)
|
||||
Reference in New Issue
Block a user