diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 6e760b47..9f3b2d64 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -828,6 +828,8 @@ "compactionEnabled": "Enable Context Compaction", "compactionDescription": "Automatically summarize older messages when context gets too large", "compactionThreshold": "Compaction Threshold (input tokens)", + "compactionRatio": "Compaction Ratio (%)", + "compactionRatioDescription": "Percentage of older messages to compact. The remaining recent messages are kept intact.", "compactionModel": "Compaction Model", "compactionModelDescription": "Select a model for summarization. Defaults to the bot's chat model if not set.", "compactionModelPlaceholder": "Use chat model (default)", diff --git a/apps/web/src/i18n/locales/zh.json b/apps/web/src/i18n/locales/zh.json index 87e8c3c7..a137a16f 100644 --- a/apps/web/src/i18n/locales/zh.json +++ b/apps/web/src/i18n/locales/zh.json @@ -824,6 +824,8 @@ "compactionEnabled": "启用上下文压缩", "compactionDescription": "上下文过大时自动摘要旧消息以节省 token", "compactionThreshold": "压缩阈值(输入 token 数)", + "compactionRatio": "压缩比例(%)", + "compactionRatioDescription": "压缩较旧消息的百分比,剩余的最近消息保持原始完整度。", "compactionModel": "压缩模型", "compactionModelDescription": "选择用于摘要的模型,未设置时默认使用聊天模型。", "compactionModelPlaceholder": "使用聊天模型(默认)", diff --git a/apps/web/src/pages/bots/components/bot-compaction.vue b/apps/web/src/pages/bots/components/bot-compaction.vue index 21e05ea2..c9004e10 100644 --- a/apps/web/src/pages/bots/components/bot-compaction.vue +++ b/apps/web/src/pages/bots/components/bot-compaction.vue @@ -28,6 +28,23 @@ :aria-label="$t('bots.settings.compactionThreshold')" /> +
+ {{ $t('bots.settings.compactionRatioDescription') }} +
+@@ -254,7 +271,7 @@ import { ref, reactive, computed, watch, onMounted } from 'vue' import { useI18n } from 'vue-i18n' import { toast } from 'vue-sonner' import { - Button, Badge, Spinner, NativeSelect, Label, Switch, Input, Separator, + Button, Badge, Spinner, NativeSelect, Label, Switch, Input, Separator, Slider, Pagination, PaginationContent, PaginationEllipsis, PaginationFirst, PaginationItem, PaginationLast, PaginationNext, PaginationPrevious, @@ -313,6 +330,7 @@ const providers = computed(() => providerData.value ?? []) const settingsForm = reactive({ compaction_enabled: false, compaction_threshold: 100000, + compaction_ratio: 80, compaction_model_id: '', }) @@ -320,6 +338,7 @@ watch(settings, (val: SettingsSettings | undefined) => { if (val) { settingsForm.compaction_enabled = val.compaction_enabled ?? false settingsForm.compaction_threshold = val.compaction_threshold ?? 100000 + settingsForm.compaction_ratio = val.compaction_ratio ?? 80 settingsForm.compaction_model_id = val.compaction_model_id ?? '' } }, { immediate: true }) @@ -329,6 +348,7 @@ const settingsChanged = computed(() => { const s: SettingsSettings = settings.value return settingsForm.compaction_enabled !== (s.compaction_enabled ?? false) || settingsForm.compaction_threshold !== (s.compaction_threshold ?? 100000) + || settingsForm.compaction_ratio !== (s.compaction_ratio ?? 80) || settingsForm.compaction_model_id !== (s.compaction_model_id ?? '') }) diff --git a/db/migrations/0001_init.up.sql b/db/migrations/0001_init.up.sql index 7cb3b0fd..260f0d39 100644 --- a/db/migrations/0001_init.up.sql +++ b/db/migrations/0001_init.up.sql @@ -174,6 +174,7 @@ CREATE TABLE IF NOT EXISTS bots ( 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_ratio INTEGER NOT NULL DEFAULT 80, 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, diff --git a/db/migrations/0052_compaction_ratio.down.sql b/db/migrations/0052_compaction_ratio.down.sql new file mode 100644 index 00000000..1d239117 --- /dev/null +++ b/db/migrations/0052_compaction_ratio.down.sql @@ -0,0 +1,4 @@ +-- 0052_compaction_ratio (rollback) +-- Remove compaction_ratio column from bots table. + +ALTER TABLE bots DROP COLUMN IF EXISTS compaction_ratio; diff --git a/db/migrations/0052_compaction_ratio.up.sql b/db/migrations/0052_compaction_ratio.up.sql new file mode 100644 index 00000000..7b2d15e3 --- /dev/null +++ b/db/migrations/0052_compaction_ratio.up.sql @@ -0,0 +1,4 @@ +-- 0052_compaction_ratio +-- Add compaction_ratio column to bots table for controlling what percentage of messages to compact. + +ALTER TABLE bots ADD COLUMN IF NOT EXISTS compaction_ratio INTEGER NOT NULL DEFAULT 80; diff --git a/db/queries/bots.sql b/db/queries/bots.sql index 08acff15..b0bc0579 100644 --- a/db/queries/bots.sql +++ b/db/queries/bots.sql @@ -4,7 +4,7 @@ VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, owner_user_id, display_name, avatar_url, timezone, is_active, status, 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, timezone, is_active, status, 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 +SELECT id, owner_user_id, display_name, avatar_url, timezone, is_active, status, 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_ratio, compaction_model_id, metadata, created_at, updated_at FROM bots WHERE id = $1; diff --git a/db/queries/settings.sql b/db/queries/settings.sql index 9dcbba05..4a67c2c7 100644 --- a/db/queries/settings.sql +++ b/db/queries/settings.sql @@ -9,6 +9,7 @@ SELECT bots.heartbeat_prompt, bots.compaction_enabled, bots.compaction_threshold, + bots.compaction_ratio, chat_models.id AS chat_model_id, heartbeat_models.id AS heartbeat_model_id, compaction_models.id AS compaction_model_id, @@ -39,6 +40,7 @@ WITH updated AS ( heartbeat_prompt = sqlc.arg(heartbeat_prompt), compaction_enabled = sqlc.arg(compaction_enabled), compaction_threshold = sqlc.arg(compaction_threshold), + compaction_ratio = sqlc.arg(compaction_ratio), 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), @@ -49,7 +51,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.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 + RETURNING bots.id, bots.language, bots.reasoning_enabled, bots.reasoning_effort, bots.heartbeat_enabled, bots.heartbeat_interval, bots.heartbeat_prompt, bots.compaction_enabled, bots.compaction_threshold, bots.compaction_ratio, 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, @@ -61,6 +63,7 @@ SELECT updated.heartbeat_prompt, updated.compaction_enabled, updated.compaction_threshold, + updated.compaction_ratio, chat_models.id AS chat_model_id, heartbeat_models.id AS heartbeat_model_id, compaction_models.id AS compaction_model_id, @@ -89,6 +92,7 @@ SET language = 'auto', heartbeat_prompt = '', compaction_enabled = false, compaction_threshold = 100000, + compaction_ratio = 80, chat_model_id = NULL, heartbeat_model_id = NULL, compaction_model_id = NULL, diff --git a/internal/acl/service_test.go b/internal/acl/service_test.go index 4f17aed0..1a103285 100644 --- a/internal/acl/service_test.go +++ b/internal/acl/service_test.go @@ -114,10 +114,11 @@ func makeBotRow(botID, ownerUserID pgtype.UUID) *fakeRow { *dest[15].(*string) = "" // HeartbeatPrompt *dest[16].(*bool) = false // CompactionEnabled *dest[17].(*int32) = 100000 // CompactionThreshold - *dest[18].(*pgtype.UUID) = pgtype.UUID{} // CompactionModelID - *dest[19].(*[]byte) = []byte(`{}`) - *dest[20].(*pgtype.Timestamptz) = pgtype.Timestamptz{} + *dest[18].(*int32) = 80 // CompactionRatio + *dest[19].(*pgtype.UUID) = pgtype.UUID{} // CompactionModelID + *dest[20].(*[]byte) = []byte(`{}`) *dest[21].(*pgtype.Timestamptz) = pgtype.Timestamptz{} + *dest[22].(*pgtype.Timestamptz) = pgtype.Timestamptz{} return nil }, } diff --git a/internal/bots/service_test.go b/internal/bots/service_test.go index cbd43b76..19ced719 100644 --- a/internal/bots/service_test.go +++ b/internal/bots/service_test.go @@ -71,10 +71,11 @@ func makeBotRow(botID, ownerUserID pgtype.UUID) *fakeRow { *dest[15].(*string) = "" // HeartbeatPrompt *dest[16].(*bool) = false // CompactionEnabled *dest[17].(*int32) = 100000 // CompactionThreshold - *dest[18].(*pgtype.UUID) = pgtype.UUID{} // CompactionModelID - *dest[19].(*[]byte) = []byte(`{}`) - *dest[20].(*pgtype.Timestamptz) = pgtype.Timestamptz{} + *dest[18].(*int32) = 80 // CompactionRatio + *dest[19].(*pgtype.UUID) = pgtype.UUID{} // CompactionModelID + *dest[20].(*[]byte) = []byte(`{}`) *dest[21].(*pgtype.Timestamptz) = pgtype.Timestamptz{} + *dest[22].(*pgtype.Timestamptz) = pgtype.Timestamptz{} return nil }, } diff --git a/internal/compaction/service.go b/internal/compaction/service.go index 132d8661..ea4cb8a6 100644 --- a/internal/compaction/service.go +++ b/internal/compaction/service.go @@ -79,6 +79,12 @@ func (s *Service) doCompaction(ctx context.Context, logID pgtype.UUID, sessionUU return nil } + toCompact := splitByRatio(messages, cfg.TotalInputTokens, cfg.Ratio) + if len(toCompact) == 0 { + s.completeLog(ctx, logID, "ok", "", "", nil, pgtype.UUID{}) + return nil + } + priorLogs, err := s.queries.ListCompactionLogsBySession(ctx, sessionUUID) if err != nil { return err @@ -90,9 +96,9 @@ func (s *Service) doCompaction(ctx context.Context, logID pgtype.UUID, sessionUU } } - entries := make([]messageEntry, 0, len(messages)) - messageIDs := make([]pgtype.UUID, 0, len(messages)) - for _, m := range messages { + entries := make([]messageEntry, 0, len(toCompact)) + messageIDs := make([]pgtype.UUID, 0, len(toCompact)) + for _, m := range toCompact { entries = append(entries, messageEntry{ Role: m.Role, Content: extractTextContent(m.Content), @@ -258,3 +264,49 @@ func extractTextContent(content []byte) string { func joinTexts(parts []string) string { return strings.Join(parts, " ") } + +// splitByRatio splits messages so that roughly the first ratio% (by token weight) +// are returned for compaction, and the rest are kept as-is. +// When ratio >= 100 or totalInputTokens <= 0, all messages are returned. +func splitByRatio(messages []sqlc.ListUncompactedMessagesBySessionRow, totalInputTokens, ratio int) []sqlc.ListUncompactedMessagesBySessionRow { + if ratio >= 100 || ratio <= 0 || totalInputTokens <= 0 || len(messages) == 0 { + return messages + } + + keepTokens := totalInputTokens * (100 - ratio) / 100 + if keepTokens <= 0 { + return messages + } + + accumulated := 0 + cutoff := len(messages) + for i := len(messages) - 1; i >= 0; i-- { + accumulated += estimateRowTokens(messages[i]) + if accumulated >= keepTokens { + cutoff = i + 1 + break + } + } + + if cutoff <= 0 { + return nil + } + if cutoff >= len(messages) { + return messages + } + return messages[:cutoff] +} + +type usagePayload struct { + OutputTokens *int `json:"output_tokens"` +} + +func estimateRowTokens(m sqlc.ListUncompactedMessagesBySessionRow) int { + if len(m.Usage) > 0 { + var u usagePayload + if json.Unmarshal(m.Usage, &u) == nil && u.OutputTokens != nil && *u.OutputTokens > 0 { + return *u.OutputTokens + } + } + return len(m.Content) / 4 +} diff --git a/internal/compaction/types.go b/internal/compaction/types.go index b1edb31b..63875d06 100644 --- a/internal/compaction/types.go +++ b/internal/compaction/types.go @@ -28,12 +28,14 @@ type ListLogsResponse struct { // TriggerConfig holds the parameters needed to trigger a compaction. type TriggerConfig struct { - BotID string - SessionID string - ModelID string - ClientType string - APIKey string //nolint:gosec // runtime credential, not a hardcoded secret - CodexAccountID string - BaseURL string - HTTPClient *http.Client + BotID string + SessionID string + ModelID string + ClientType string + APIKey string //nolint:gosec // runtime credential, not a hardcoded secret + CodexAccountID string + BaseURL string + HTTPClient *http.Client + Ratio int + TotalInputTokens int } diff --git a/internal/conversation/flow/resolver_compaction.go b/internal/conversation/flow/resolver_compaction.go index f09c7ab1..9fcbf839 100644 --- a/internal/conversation/flow/resolver_compaction.go +++ b/internal/conversation/flow/resolver_compaction.go @@ -31,9 +31,16 @@ func (r *Resolver) maybeCompact(ctx context.Context, req conversation.ChatReques modelID = rc.model.ID } + ratio := settings.CompactionRatio + if ratio <= 0 || ratio > 100 { + ratio = 80 + } + cfg := compaction.TriggerConfig{ - BotID: req.BotID, - SessionID: req.SessionID, + BotID: req.BotID, + SessionID: req.SessionID, + Ratio: ratio, + TotalInputTokens: inputTokens, } model, err := r.modelsService.GetByID(ctx, modelID) diff --git a/internal/db/sqlc/bots.sql.go b/internal/db/sqlc/bots.sql.go index 6613ea63..5f576f24 100644 --- a/internal/db/sqlc/bots.sql.go +++ b/internal/db/sqlc/bots.sql.go @@ -94,7 +94,7 @@ func (q *Queries) DeleteBotByID(ctx context.Context, id pgtype.UUID) error { } const getBotByID = `-- name: GetBotByID :one -SELECT id, owner_user_id, display_name, avatar_url, timezone, is_active, status, 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 +SELECT id, owner_user_id, display_name, avatar_url, timezone, is_active, status, 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_ratio, compaction_model_id, metadata, created_at, updated_at FROM bots WHERE id = $1 ` @@ -118,6 +118,7 @@ type GetBotByIDRow struct { HeartbeatPrompt string `json:"heartbeat_prompt"` CompactionEnabled bool `json:"compaction_enabled"` CompactionThreshold int32 `json:"compaction_threshold"` + CompactionRatio int32 `json:"compaction_ratio"` CompactionModelID pgtype.UUID `json:"compaction_model_id"` Metadata []byte `json:"metadata"` CreatedAt pgtype.Timestamptz `json:"created_at"` @@ -146,6 +147,7 @@ func (q *Queries) GetBotByID(ctx context.Context, id pgtype.UUID) (GetBotByIDRow &i.HeartbeatPrompt, &i.CompactionEnabled, &i.CompactionThreshold, + &i.CompactionRatio, &i.CompactionModelID, &i.Metadata, &i.CreatedAt, diff --git a/internal/db/sqlc/conversations.sql.go b/internal/db/sqlc/conversations.sql.go index 62f6404e..6b7f50bb 100644 --- a/internal/db/sqlc/conversations.sql.go +++ b/internal/db/sqlc/conversations.sql.go @@ -511,7 +511,7 @@ WITH updated AS ( SET display_name = $1, updated_at = now() WHERE bots.id = $2 - RETURNING id, owner_user_id, display_name, avatar_url, timezone, is_active, status, language, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, heartbeat_model_id, compaction_enabled, compaction_threshold, compaction_model_id, title_model_id, tts_model_id, browser_context_id, metadata, created_at, updated_at, acl_default_effect + RETURNING id, owner_user_id, display_name, avatar_url, timezone, is_active, status, language, reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id, heartbeat_enabled, heartbeat_interval, heartbeat_prompt, heartbeat_model_id, compaction_enabled, compaction_threshold, compaction_ratio, compaction_model_id, title_model_id, tts_model_id, browser_context_id, metadata, created_at, updated_at, acl_default_effect ) SELECT updated.id AS id, diff --git a/internal/db/sqlc/models.go b/internal/db/sqlc/models.go index 0a633fa3..53204cfc 100644 --- a/internal/db/sqlc/models.go +++ b/internal/db/sqlc/models.go @@ -28,6 +28,7 @@ type Bot struct { HeartbeatModelID pgtype.UUID `json:"heartbeat_model_id"` CompactionEnabled bool `json:"compaction_enabled"` CompactionThreshold int32 `json:"compaction_threshold"` + CompactionRatio int32 `json:"compaction_ratio"` CompactionModelID pgtype.UUID `json:"compaction_model_id"` TitleModelID pgtype.UUID `json:"title_model_id"` TtsModelID pgtype.UUID `json:"tts_model_id"` diff --git a/internal/db/sqlc/settings.sql.go b/internal/db/sqlc/settings.sql.go index 77603778..11b2ead8 100644 --- a/internal/db/sqlc/settings.sql.go +++ b/internal/db/sqlc/settings.sql.go @@ -21,6 +21,7 @@ SET language = 'auto', heartbeat_prompt = '', compaction_enabled = false, compaction_threshold = 100000, + compaction_ratio = 80, chat_model_id = NULL, heartbeat_model_id = NULL, compaction_model_id = NULL, @@ -49,6 +50,7 @@ SELECT bots.heartbeat_prompt, bots.compaction_enabled, bots.compaction_threshold, + bots.compaction_ratio, chat_models.id AS chat_model_id, heartbeat_models.id AS heartbeat_model_id, compaction_models.id AS compaction_model_id, @@ -79,6 +81,7 @@ type GetSettingsByBotIDRow struct { HeartbeatPrompt string `json:"heartbeat_prompt"` CompactionEnabled bool `json:"compaction_enabled"` CompactionThreshold int32 `json:"compaction_threshold"` + CompactionRatio int32 `json:"compaction_ratio"` ChatModelID pgtype.UUID `json:"chat_model_id"` HeartbeatModelID pgtype.UUID `json:"heartbeat_model_id"` CompactionModelID pgtype.UUID `json:"compaction_model_id"` @@ -102,6 +105,7 @@ func (q *Queries) GetSettingsByBotID(ctx context.Context, id pgtype.UUID) (GetSe &i.HeartbeatPrompt, &i.CompactionEnabled, &i.CompactionThreshold, + &i.CompactionRatio, &i.ChatModelID, &i.HeartbeatModelID, &i.CompactionModelID, @@ -125,17 +129,18 @@ WITH updated AS ( heartbeat_prompt = $6, compaction_enabled = $7, compaction_threshold = $8, - chat_model_id = COALESCE($9::uuid, bots.chat_model_id), - heartbeat_model_id = COALESCE($10::uuid, bots.heartbeat_model_id), - compaction_model_id = COALESCE($11::uuid, bots.compaction_model_id), - title_model_id = COALESCE($12::uuid, bots.title_model_id), - search_provider_id = COALESCE($13::uuid, bots.search_provider_id), - memory_provider_id = COALESCE($14::uuid, bots.memory_provider_id), - tts_model_id = COALESCE($15::uuid, bots.tts_model_id), - browser_context_id = COALESCE($16::uuid, bots.browser_context_id), + compaction_ratio = $9, + chat_model_id = COALESCE($10::uuid, bots.chat_model_id), + heartbeat_model_id = COALESCE($11::uuid, bots.heartbeat_model_id), + compaction_model_id = COALESCE($12::uuid, bots.compaction_model_id), + title_model_id = COALESCE($13::uuid, bots.title_model_id), + search_provider_id = COALESCE($14::uuid, bots.search_provider_id), + memory_provider_id = COALESCE($15::uuid, bots.memory_provider_id), + tts_model_id = COALESCE($16::uuid, bots.tts_model_id), + browser_context_id = COALESCE($17::uuid, bots.browser_context_id), updated_at = now() - WHERE bots.id = $17 - RETURNING bots.id, 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 + WHERE bots.id = $18 + RETURNING bots.id, bots.language, bots.reasoning_enabled, bots.reasoning_effort, bots.heartbeat_enabled, bots.heartbeat_interval, bots.heartbeat_prompt, bots.compaction_enabled, bots.compaction_threshold, bots.compaction_ratio, 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, @@ -147,6 +152,7 @@ SELECT updated.heartbeat_prompt, updated.compaction_enabled, updated.compaction_threshold, + updated.compaction_ratio, chat_models.id AS chat_model_id, heartbeat_models.id AS heartbeat_model_id, compaction_models.id AS compaction_model_id, @@ -175,6 +181,7 @@ type UpsertBotSettingsParams struct { HeartbeatPrompt string `json:"heartbeat_prompt"` CompactionEnabled bool `json:"compaction_enabled"` CompactionThreshold int32 `json:"compaction_threshold"` + CompactionRatio int32 `json:"compaction_ratio"` ChatModelID pgtype.UUID `json:"chat_model_id"` HeartbeatModelID pgtype.UUID `json:"heartbeat_model_id"` CompactionModelID pgtype.UUID `json:"compaction_model_id"` @@ -196,6 +203,7 @@ type UpsertBotSettingsRow struct { HeartbeatPrompt string `json:"heartbeat_prompt"` CompactionEnabled bool `json:"compaction_enabled"` CompactionThreshold int32 `json:"compaction_threshold"` + CompactionRatio int32 `json:"compaction_ratio"` ChatModelID pgtype.UUID `json:"chat_model_id"` HeartbeatModelID pgtype.UUID `json:"heartbeat_model_id"` CompactionModelID pgtype.UUID `json:"compaction_model_id"` @@ -216,6 +224,7 @@ func (q *Queries) UpsertBotSettings(ctx context.Context, arg UpsertBotSettingsPa arg.HeartbeatPrompt, arg.CompactionEnabled, arg.CompactionThreshold, + arg.CompactionRatio, arg.ChatModelID, arg.HeartbeatModelID, arg.CompactionModelID, @@ -237,6 +246,7 @@ func (q *Queries) UpsertBotSettings(ctx context.Context, arg UpsertBotSettingsPa &i.HeartbeatPrompt, &i.CompactionEnabled, &i.CompactionThreshold, + &i.CompactionRatio, &i.ChatModelID, &i.HeartbeatModelID, &i.CompactionModelID, diff --git a/internal/settings/service.go b/internal/settings/service.go index a7724a0f..8adf86df 100644 --- a/internal/settings/service.go +++ b/internal/settings/service.go @@ -69,7 +69,7 @@ func (s *Service) UpsertBot(ctx context.Context, botID string, req UpsertRequest if err != nil { return Settings{}, err } - current := normalizeBotSetting(botRow.Language, aclDefaultEffect, botRow.ReasoningEnabled, botRow.ReasoningEffort, botRow.HeartbeatEnabled, botRow.HeartbeatInterval, botRow.CompactionEnabled, botRow.CompactionThreshold) + current := normalizeBotSetting(botRow.Language, aclDefaultEffect, botRow.ReasoningEnabled, botRow.ReasoningEffort, botRow.HeartbeatEnabled, botRow.HeartbeatInterval, botRow.CompactionEnabled, botRow.CompactionThreshold, botRow.CompactionRatio) if strings.TrimSpace(req.Language) != "" { current.Language = strings.TrimSpace(req.Language) } @@ -94,6 +94,9 @@ func (s *Service) UpsertBot(ctx context.Context, botID string, req UpsertRequest if req.CompactionThreshold != nil && *req.CompactionThreshold >= 0 { current.CompactionThreshold = *req.CompactionThreshold } + if req.CompactionRatio != nil && *req.CompactionRatio >= 1 && *req.CompactionRatio <= 100 { + current.CompactionRatio = *req.CompactionRatio + } chatModelUUID := pgtype.UUID{} if value := strings.TrimSpace(req.ChatModelID); value != "" { modelID, err := s.resolveModelUUID(ctx, value) @@ -170,6 +173,7 @@ func (s *Service) UpsertBot(ctx context.Context, botID string, req UpsertRequest HeartbeatPrompt: "", CompactionEnabled: current.CompactionEnabled, CompactionThreshold: int32(current.CompactionThreshold), //nolint:gosec // bounded by non-negative setter above + CompactionRatio: int32(current.CompactionRatio), //nolint:gosec // bounded 1-100 above ChatModelID: chatModelUUID, HeartbeatModelID: heartbeatModelUUID, CompactionModelID: compactionModelUUID, @@ -209,7 +213,7 @@ func (s *Service) Delete(ctx context.Context, botID string) error { return nil } -func normalizeBotSetting(language string, aclDefaultEffect string, reasoningEnabled bool, reasoningEffort string, heartbeatEnabled bool, heartbeatInterval int32, compactionEnabled bool, compactionThreshold int32) Settings { +func normalizeBotSetting(language string, aclDefaultEffect string, reasoningEnabled bool, reasoningEffort string, heartbeatEnabled bool, heartbeatInterval int32, compactionEnabled bool, compactionThreshold int32, compactionRatio int32) Settings { settings := Settings{ Language: strings.TrimSpace(language), AclDefaultEffect: strings.TrimSpace(aclDefaultEffect), @@ -219,6 +223,7 @@ func normalizeBotSetting(language string, aclDefaultEffect string, reasoningEnab HeartbeatInterval: int(heartbeatInterval), CompactionEnabled: compactionEnabled, CompactionThreshold: int(compactionThreshold), + CompactionRatio: int(compactionRatio), } if settings.Language == "" { settings.Language = DefaultLanguage @@ -235,6 +240,9 @@ func normalizeBotSetting(language string, aclDefaultEffect string, reasoningEnab if settings.CompactionThreshold < 0 { settings.CompactionThreshold = 0 } + if settings.CompactionRatio < 1 || settings.CompactionRatio > 100 { + settings.CompactionRatio = 80 + } return settings } @@ -256,6 +264,7 @@ func normalizeBotSettingsReadRow(row sqlc.GetSettingsByBotIDRow) Settings { row.HeartbeatInterval, row.CompactionEnabled, row.CompactionThreshold, + row.CompactionRatio, row.ChatModelID, row.HeartbeatModelID, row.CompactionModelID, @@ -276,6 +285,7 @@ func normalizeBotSettingsWriteRow(row sqlc.UpsertBotSettingsRow) Settings { row.HeartbeatInterval, row.CompactionEnabled, row.CompactionThreshold, + row.CompactionRatio, row.ChatModelID, row.HeartbeatModelID, row.CompactionModelID, @@ -295,6 +305,7 @@ func normalizeBotSettingsFields( heartbeatInterval int32, compactionEnabled bool, compactionThreshold int32, + compactionRatio int32, chatModelID pgtype.UUID, heartbeatModelID pgtype.UUID, compactionModelID pgtype.UUID, @@ -304,7 +315,7 @@ func normalizeBotSettingsFields( ttsModelID pgtype.UUID, browserContextID pgtype.UUID, ) Settings { - settings := normalizeBotSetting(language, "", reasoningEnabled, reasoningEffort, heartbeatEnabled, heartbeatInterval, compactionEnabled, compactionThreshold) + settings := normalizeBotSetting(language, "", reasoningEnabled, reasoningEffort, heartbeatEnabled, heartbeatInterval, compactionEnabled, compactionThreshold, compactionRatio) if chatModelID.Valid { settings.ChatModelID = uuid.UUID(chatModelID.Bytes).String() } diff --git a/internal/settings/types.go b/internal/settings/types.go index bd371c45..9cc9835b 100644 --- a/internal/settings/types.go +++ b/internal/settings/types.go @@ -22,6 +22,7 @@ type Settings struct { TitleModelID string `json:"title_model_id"` CompactionEnabled bool `json:"compaction_enabled"` CompactionThreshold int `json:"compaction_threshold"` + CompactionRatio int `json:"compaction_ratio"` CompactionModelID string `json:"compaction_model_id,omitempty"` } @@ -41,5 +42,6 @@ type UpsertRequest struct { TitleModelID string `json:"title_model_id,omitempty"` CompactionEnabled *bool `json:"compaction_enabled,omitempty"` CompactionThreshold *int `json:"compaction_threshold,omitempty"` + CompactionRatio *int `json:"compaction_ratio,omitempty"` CompactionModelID *string `json:"compaction_model_id,omitempty"` } diff --git a/packages/sdk/src/types.gen.ts b/packages/sdk/src/types.gen.ts index cd188d2d..179dee37 100644 --- a/packages/sdk/src/types.gen.ts +++ b/packages/sdk/src/types.gen.ts @@ -1447,6 +1447,7 @@ export type SettingsSettings = { chat_model_id?: string; compaction_enabled?: boolean; compaction_model_id?: string; + compaction_ratio?: number; compaction_threshold?: number; heartbeat_enabled?: boolean; heartbeat_interval?: number; @@ -1466,6 +1467,7 @@ export type SettingsUpsertRequest = { chat_model_id?: string; compaction_enabled?: boolean; compaction_model_id?: string; + compaction_ratio?: number; compaction_threshold?: number; heartbeat_enabled?: boolean; heartbeat_interval?: number; diff --git a/spec/docs.go b/spec/docs.go index ffa334e0..0e82f3c8 100644 --- a/spec/docs.go +++ b/spec/docs.go @@ -12401,6 +12401,9 @@ const docTemplate = `{ "compaction_model_id": { "type": "string" }, + "compaction_ratio": { + "type": "integer" + }, "compaction_threshold": { "type": "integer" }, @@ -12454,6 +12457,9 @@ const docTemplate = `{ "compaction_model_id": { "type": "string" }, + "compaction_ratio": { + "type": "integer" + }, "compaction_threshold": { "type": "integer" }, diff --git a/spec/swagger.json b/spec/swagger.json index eb61d66f..dece8a0a 100644 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -12392,6 +12392,9 @@ "compaction_model_id": { "type": "string" }, + "compaction_ratio": { + "type": "integer" + }, "compaction_threshold": { "type": "integer" }, @@ -12445,6 +12448,9 @@ "compaction_model_id": { "type": "string" }, + "compaction_ratio": { + "type": "integer" + }, "compaction_threshold": { "type": "integer" }, diff --git a/spec/swagger.yaml b/spec/swagger.yaml index aaac48da..53b14836 100644 --- a/spec/swagger.yaml +++ b/spec/swagger.yaml @@ -2398,6 +2398,8 @@ definitions: type: boolean compaction_model_id: type: string + compaction_ratio: + type: integer compaction_threshold: type: integer heartbeat_enabled: @@ -2433,6 +2435,8 @@ definitions: type: boolean compaction_model_id: type: string + compaction_ratio: + type: integer compaction_threshold: type: integer heartbeat_enabled: