mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: add context compaction to automatically summarize old messages (#compaction) (#276)
When input tokens exceed a configurable threshold after a conversation round, the system asynchronously compacts older messages into a summary. Cascading compactions reference prior summaries via <prior_context> tags to maintain conversational continuity without duplicating content. - Add bot_history_message_compacts table and compact_id on messages - Add compaction_enabled, compaction_threshold, compaction_model_id to bots - Implement compaction service (internal/compaction) with LLM summarization - Integrate into conversation flow: replace compacted messages with summaries wrapped in <summary> tags during context loading - Add REST API endpoints (GET/DELETE /bots/:bot_id/compaction/logs) - Add frontend Compaction tab with settings and log viewer - Wire compaction service into both dev (cmd/agent) and prod (cmd/memoh) entry points - Update test mocks to include new GetBotByID columns
This commit is contained in:
@@ -172,6 +172,9 @@ CREATE TABLE IF NOT EXISTS bots (
|
||||
heartbeat_interval INTEGER NOT NULL DEFAULT 30,
|
||||
heartbeat_prompt TEXT NOT NULL DEFAULT '',
|
||||
heartbeat_model_id UUID REFERENCES models(id) ON DELETE SET NULL,
|
||||
compaction_enabled BOOLEAN NOT NULL DEFAULT false,
|
||||
compaction_threshold INTEGER NOT NULL DEFAULT 100000,
|
||||
compaction_model_id UUID REFERENCES models(id) ON DELETE SET NULL,
|
||||
title_model_id UUID REFERENCES models(id) ON DELETE SET NULL,
|
||||
tts_model_id UUID REFERENCES tts_models(id) ON DELETE SET NULL,
|
||||
browser_context_id UUID REFERENCES browser_contexts(id) ON DELETE SET NULL,
|
||||
@@ -373,10 +376,12 @@ CREATE TABLE IF NOT EXISTS bot_history_messages (
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
usage JSONB,
|
||||
model_id UUID REFERENCES models(id) ON DELETE SET NULL,
|
||||
compact_id UUID,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_bot_created ON bot_history_messages(bot_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_compact ON bot_history_messages(compact_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_session
|
||||
ON bot_history_messages(session_id, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_session_source
|
||||
@@ -539,6 +544,23 @@ CREATE TABLE IF NOT EXISTS bot_heartbeat_logs (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_heartbeat_logs_bot_started ON bot_heartbeat_logs(bot_id, started_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bot_history_message_compacts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
|
||||
session_id UUID REFERENCES bot_sessions(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'ok', 'error')),
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
message_count INTEGER NOT NULL DEFAULT 0,
|
||||
error_message TEXT NOT NULL DEFAULT '',
|
||||
usage JSONB,
|
||||
model_id UUID REFERENCES models(id) ON DELETE SET NULL,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_compacts_bot_session ON bot_history_message_compacts(bot_id, session_id, started_at DESC);
|
||||
|
||||
ALTER TABLE bot_history_messages ADD CONSTRAINT fk_compact_id FOREIGN KEY (compact_id) REFERENCES bot_history_message_compacts(id) ON DELETE SET NULL;
|
||||
|
||||
-- schedule_logs: structured execution records for scheduled tasks.
|
||||
CREATE TABLE IF NOT EXISTS schedule_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 0040_compaction (down)
|
||||
-- Revert context compaction support.
|
||||
|
||||
ALTER TABLE bots DROP COLUMN IF EXISTS compaction_model_id;
|
||||
ALTER TABLE bots DROP COLUMN IF EXISTS compaction_threshold;
|
||||
ALTER TABLE bots DROP COLUMN IF EXISTS compaction_enabled;
|
||||
|
||||
DROP INDEX IF EXISTS idx_bot_history_messages_compact;
|
||||
ALTER TABLE bot_history_messages DROP COLUMN IF EXISTS compact_id;
|
||||
|
||||
DROP INDEX IF EXISTS idx_compacts_bot_session;
|
||||
DROP TABLE IF EXISTS bot_history_message_compacts;
|
||||
@@ -0,0 +1,28 @@
|
||||
-- 0040_compaction
|
||||
-- Add context compaction support: compaction logs table, compact_id on messages, bot settings columns.
|
||||
|
||||
-- bot_history_message_compacts: stores compaction records and summaries.
|
||||
CREATE TABLE IF NOT EXISTS bot_history_message_compacts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
|
||||
session_id UUID REFERENCES bot_sessions(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'ok', 'error')),
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
message_count INTEGER NOT NULL DEFAULT 0,
|
||||
error_message TEXT NOT NULL DEFAULT '',
|
||||
usage JSONB,
|
||||
model_id UUID REFERENCES models(id) ON DELETE SET NULL,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_compacts_bot_session ON bot_history_message_compacts(bot_id, session_id, started_at DESC);
|
||||
|
||||
-- Add compact_id to messages to track which compaction a message belongs to.
|
||||
ALTER TABLE bot_history_messages ADD COLUMN IF NOT EXISTS compact_id UUID REFERENCES bot_history_message_compacts(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_compact ON bot_history_messages(compact_id);
|
||||
|
||||
-- Bot-level compaction settings.
|
||||
ALTER TABLE bots ADD COLUMN IF NOT EXISTS compaction_enabled BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE bots ADD COLUMN IF NOT EXISTS compaction_threshold INTEGER NOT NULL DEFAULT 100000;
|
||||
ALTER TABLE bots ADD COLUMN IF NOT EXISTS compaction_model_id UUID REFERENCES models(id) ON DELETE SET NULL;
|
||||
+1
-1
@@ -4,7 +4,7 @@ VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, owner_user_id, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at;
|
||||
|
||||
-- name: GetBotByID :one
|
||||
SELECT id, owner_user_id, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at
|
||||
SELECT id, owner_user_id, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, compaction_enabled, compaction_threshold, compaction_model_id, metadata, created_at, updated_at
|
||||
FROM bots
|
||||
WHERE id = $1;
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
-- name: CreateCompactionLog :one
|
||||
INSERT INTO bot_history_message_compacts (bot_id, session_id)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, bot_id, session_id, status, summary, message_count, error_message, usage, model_id, started_at, completed_at;
|
||||
|
||||
-- name: CompleteCompactionLog :one
|
||||
UPDATE bot_history_message_compacts
|
||||
SET status = $2,
|
||||
summary = $3,
|
||||
message_count = $4,
|
||||
error_message = $5,
|
||||
usage = $6,
|
||||
model_id = $7,
|
||||
completed_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING id, bot_id, session_id, status, summary, message_count, error_message, usage, model_id, started_at, completed_at;
|
||||
|
||||
-- name: GetCompactionLogByID :one
|
||||
SELECT id, bot_id, session_id, status, summary, message_count, error_message, usage, model_id, started_at, completed_at
|
||||
FROM bot_history_message_compacts
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListCompactionLogsByBot :many
|
||||
SELECT id, bot_id, session_id, status, summary, message_count, error_message, usage, model_id, started_at, completed_at
|
||||
FROM bot_history_message_compacts
|
||||
WHERE bot_id = $1
|
||||
AND ($2::timestamptz IS NULL OR started_at < $2)
|
||||
ORDER BY started_at DESC
|
||||
LIMIT $3;
|
||||
|
||||
-- name: ListCompactionLogsBySession :many
|
||||
SELECT id, bot_id, session_id, status, summary, message_count, error_message, usage, model_id, started_at, completed_at
|
||||
FROM bot_history_message_compacts
|
||||
WHERE session_id = $1
|
||||
ORDER BY started_at ASC;
|
||||
|
||||
-- name: DeleteCompactionLogsByBot :exec
|
||||
DELETE FROM bot_history_message_compacts WHERE bot_id = $1;
|
||||
@@ -148,6 +148,7 @@ SELECT
|
||||
m.content,
|
||||
m.metadata,
|
||||
m.usage,
|
||||
m.compact_id,
|
||||
m.created_at,
|
||||
ci.display_name AS sender_display_name,
|
||||
ci.avatar_url AS sender_avatar_url,
|
||||
@@ -173,6 +174,7 @@ SELECT
|
||||
m.content,
|
||||
m.metadata,
|
||||
m.usage,
|
||||
m.compact_id,
|
||||
m.created_at,
|
||||
ci.display_name AS sender_display_name,
|
||||
ci.avatar_url AS sender_avatar_url,
|
||||
@@ -360,3 +362,16 @@ WHERE m.bot_id = sqlc.arg(bot_id)
|
||||
) ILIKE '%' || sqlc.narg(keyword)::text || '%')
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT sqlc.arg(max_count);
|
||||
|
||||
-- name: MarkMessagesCompacted :exec
|
||||
UPDATE bot_history_messages
|
||||
SET compact_id = $1
|
||||
WHERE id = ANY($2::uuid[]);
|
||||
|
||||
-- name: ListUncompactedMessagesBySession :many
|
||||
SELECT id, chat_id, session_id, role, content, usage, platform, external_message_id, sender_channel_identity_id, compact_id, created_at
|
||||
FROM bot_history_messages
|
||||
WHERE session_id = $1
|
||||
AND compact_id IS NULL
|
||||
AND is_active = true
|
||||
ORDER BY created_at ASC;
|
||||
|
||||
+15
-1
@@ -9,8 +9,11 @@ SELECT
|
||||
bots.heartbeat_enabled,
|
||||
bots.heartbeat_interval,
|
||||
bots.heartbeat_prompt,
|
||||
bots.compaction_enabled,
|
||||
bots.compaction_threshold,
|
||||
chat_models.id AS chat_model_id,
|
||||
heartbeat_models.id AS heartbeat_model_id,
|
||||
compaction_models.id AS compaction_model_id,
|
||||
title_models.id AS title_model_id,
|
||||
search_providers.id AS search_provider_id,
|
||||
memory_providers.id AS memory_provider_id,
|
||||
@@ -19,6 +22,7 @@ SELECT
|
||||
FROM bots
|
||||
LEFT JOIN models AS chat_models ON chat_models.id = bots.chat_model_id
|
||||
LEFT JOIN models AS heartbeat_models ON heartbeat_models.id = bots.heartbeat_model_id
|
||||
LEFT JOIN models AS compaction_models ON compaction_models.id = bots.compaction_model_id
|
||||
LEFT JOIN models AS title_models ON title_models.id = bots.title_model_id
|
||||
LEFT JOIN search_providers ON search_providers.id = bots.search_provider_id
|
||||
LEFT JOIN memory_providers ON memory_providers.id = bots.memory_provider_id
|
||||
@@ -37,8 +41,11 @@ WITH updated AS (
|
||||
heartbeat_enabled = sqlc.arg(heartbeat_enabled),
|
||||
heartbeat_interval = sqlc.arg(heartbeat_interval),
|
||||
heartbeat_prompt = sqlc.arg(heartbeat_prompt),
|
||||
compaction_enabled = sqlc.arg(compaction_enabled),
|
||||
compaction_threshold = sqlc.arg(compaction_threshold),
|
||||
chat_model_id = COALESCE(sqlc.narg(chat_model_id)::uuid, bots.chat_model_id),
|
||||
heartbeat_model_id = COALESCE(sqlc.narg(heartbeat_model_id)::uuid, bots.heartbeat_model_id),
|
||||
compaction_model_id = COALESCE(sqlc.narg(compaction_model_id)::uuid, bots.compaction_model_id),
|
||||
title_model_id = COALESCE(sqlc.narg(title_model_id)::uuid, bots.title_model_id),
|
||||
search_provider_id = COALESCE(sqlc.narg(search_provider_id)::uuid, bots.search_provider_id),
|
||||
memory_provider_id = COALESCE(sqlc.narg(memory_provider_id)::uuid, bots.memory_provider_id),
|
||||
@@ -46,7 +53,7 @@ WITH updated AS (
|
||||
browser_context_id = COALESCE(sqlc.narg(browser_context_id)::uuid, bots.browser_context_id),
|
||||
updated_at = now()
|
||||
WHERE bots.id = sqlc.arg(id)
|
||||
RETURNING bots.id, bots.max_context_load_time, bots.max_context_tokens, bots.language, bots.reasoning_enabled, bots.reasoning_effort, bots.heartbeat_enabled, bots.heartbeat_interval, bots.heartbeat_prompt, bots.chat_model_id, bots.heartbeat_model_id, bots.title_model_id, bots.search_provider_id, bots.memory_provider_id, bots.tts_model_id, bots.browser_context_id
|
||||
RETURNING bots.id, bots.max_context_load_time, bots.max_context_tokens, bots.language, bots.reasoning_enabled, bots.reasoning_effort, bots.heartbeat_enabled, bots.heartbeat_interval, bots.heartbeat_prompt, bots.compaction_enabled, bots.compaction_threshold, bots.chat_model_id, bots.heartbeat_model_id, bots.compaction_model_id, bots.title_model_id, bots.search_provider_id, bots.memory_provider_id, bots.tts_model_id, bots.browser_context_id
|
||||
)
|
||||
SELECT
|
||||
updated.id AS bot_id,
|
||||
@@ -58,8 +65,11 @@ SELECT
|
||||
updated.heartbeat_enabled,
|
||||
updated.heartbeat_interval,
|
||||
updated.heartbeat_prompt,
|
||||
updated.compaction_enabled,
|
||||
updated.compaction_threshold,
|
||||
chat_models.id AS chat_model_id,
|
||||
heartbeat_models.id AS heartbeat_model_id,
|
||||
compaction_models.id AS compaction_model_id,
|
||||
title_models.id AS title_model_id,
|
||||
search_providers.id AS search_provider_id,
|
||||
memory_providers.id AS memory_provider_id,
|
||||
@@ -68,6 +78,7 @@ SELECT
|
||||
FROM updated
|
||||
LEFT JOIN models AS chat_models ON chat_models.id = updated.chat_model_id
|
||||
LEFT JOIN models AS heartbeat_models ON heartbeat_models.id = updated.heartbeat_model_id
|
||||
LEFT JOIN models AS compaction_models ON compaction_models.id = updated.compaction_model_id
|
||||
LEFT JOIN models AS title_models ON title_models.id = updated.title_model_id
|
||||
LEFT JOIN search_providers ON search_providers.id = updated.search_provider_id
|
||||
LEFT JOIN memory_providers ON memory_providers.id = updated.memory_provider_id
|
||||
@@ -84,8 +95,11 @@ SET max_context_load_time = 1440,
|
||||
heartbeat_enabled = false,
|
||||
heartbeat_interval = 30,
|
||||
heartbeat_prompt = '',
|
||||
compaction_enabled = false,
|
||||
compaction_threshold = 100000,
|
||||
chat_model_id = NULL,
|
||||
heartbeat_model_id = NULL,
|
||||
compaction_model_id = NULL,
|
||||
title_model_id = NULL,
|
||||
search_provider_id = NULL,
|
||||
memory_provider_id = NULL,
|
||||
|
||||
Reference in New Issue
Block a user