feat: max context tokens

This commit is contained in:
Acbox
2026-02-18 17:20:22 +08:00
parent 46e8a48ab8
commit 77e9f585a1
26 changed files with 277 additions and 51 deletions
BIN
View File
Binary file not shown.
+2
View File
@@ -117,6 +117,7 @@ CREATE TABLE IF NOT EXISTS bots (
is_active BOOLEAN NOT NULL DEFAULT true,
status TEXT NOT NULL DEFAULT 'ready',
max_context_load_time INTEGER NOT NULL DEFAULT 1440,
max_context_tokens INTEGER NOT NULL DEFAULT 0,
language TEXT NOT NULL DEFAULT 'auto',
allow_guest BOOLEAN NOT NULL DEFAULT false,
chat_model_id UUID REFERENCES models(id) ON DELETE SET NULL,
@@ -242,6 +243,7 @@ CREATE TABLE IF NOT EXISTS bot_history_messages (
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'tool')),
content JSONB NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
usage JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
@@ -0,0 +1,4 @@
-- 0008_max_context_tokens
-- Remove max_context_tokens column from bots table
ALTER TABLE bots DROP COLUMN IF EXISTS max_context_tokens;
@@ -0,0 +1,4 @@
-- 0008_max_context_tokens
-- Add max_context_tokens column to bots table for token-based context trimming
ALTER TABLE bots ADD COLUMN IF NOT EXISTS max_context_tokens INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,4 @@
-- 0009_message_usage
-- Remove usage JSONB column from bot_history_messages
ALTER TABLE bot_history_messages DROP COLUMN IF EXISTS usage;
+4
View File
@@ -0,0 +1,4 @@
-- 0009_message_usage
-- Add usage JSONB column to bot_history_messages for storing LLM token usage
ALTER TABLE bot_history_messages ADD COLUMN IF NOT EXISTS usage JSONB;
+6 -6
View File
@@ -1,21 +1,21 @@
-- name: CreateBot :one
INSERT INTO bots (owner_user_id, type, display_name, avatar_url, is_active, metadata, status)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at;
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at;
-- name: GetBotByID :one
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
FROM bots
WHERE id = $1;
-- name: ListBotsByOwner :many
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
FROM bots
WHERE owner_user_id = $1
ORDER BY created_at DESC;
-- name: ListBotsByMember :many
SELECT b.id, b.owner_user_id, b.type, b.display_name, b.avatar_url, b.is_active, b.status, b.max_context_load_time, b.language, b.allow_guest, b.chat_model_id, b.memory_model_id, b.embedding_model_id, b.search_provider_id, b.metadata, b.created_at, b.updated_at
SELECT b.id, b.owner_user_id, b.type, b.display_name, b.avatar_url, b.is_active, b.status, b.max_context_load_time, b.max_context_tokens, b.language, b.allow_guest, b.chat_model_id, b.memory_model_id, b.embedding_model_id, b.search_provider_id, b.metadata, b.created_at, b.updated_at
FROM bots b
JOIN bot_members m ON m.bot_id = b.id
WHERE m.user_id = $1
@@ -29,14 +29,14 @@ SET display_name = $2,
metadata = $5,
updated_at = now()
WHERE id = $1
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at;
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at;
-- name: UpdateBotOwner :one
UPDATE bots
SET owner_user_id = $2,
updated_at = now()
WHERE id = $1
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at;
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at;
-- name: UpdateBotStatus :exec
UPDATE bots
+9 -2
View File
@@ -9,7 +9,8 @@ INSERT INTO bot_history_messages (
source_reply_to_message_id,
role,
content,
metadata
metadata,
usage
)
VALUES (
sqlc.arg(bot_id),
@@ -21,7 +22,8 @@ VALUES (
sqlc.narg(source_reply_to_message_id)::text,
sqlc.arg(role),
sqlc.arg(content),
sqlc.arg(metadata)
sqlc.arg(metadata),
sqlc.arg(usage)
)
RETURNING
id,
@@ -35,6 +37,7 @@ RETURNING
role,
content,
metadata,
usage,
created_at;
-- name: ListMessages :many
@@ -50,6 +53,7 @@ SELECT
m.role,
m.content,
m.metadata,
m.usage,
m.created_at,
ci.display_name AS sender_display_name,
ci.avatar_url AS sender_avatar_url
@@ -72,6 +76,7 @@ SELECT
m.role,
m.content,
m.metadata,
m.usage,
m.created_at,
ci.display_name AS sender_display_name,
ci.avatar_url AS sender_avatar_url
@@ -94,6 +99,7 @@ SELECT
m.role,
m.content,
m.metadata,
m.usage,
m.created_at,
ci.display_name AS sender_display_name,
ci.avatar_url AS sender_avatar_url
@@ -117,6 +123,7 @@ SELECT
m.role,
m.content,
m.metadata,
m.usage,
m.created_at,
ci.display_name AS sender_display_name,
ci.avatar_url AS sender_avatar_url
+5 -1
View File
@@ -2,6 +2,7 @@
SELECT
bots.id AS bot_id,
bots.max_context_load_time,
bots.max_context_tokens,
bots.language,
bots.allow_guest,
chat_models.model_id AS chat_model_id,
@@ -19,6 +20,7 @@ WHERE bots.id = $1;
WITH updated AS (
UPDATE bots
SET max_context_load_time = sqlc.arg(max_context_load_time),
max_context_tokens = sqlc.arg(max_context_tokens),
language = sqlc.arg(language),
allow_guest = sqlc.arg(allow_guest),
chat_model_id = COALESCE(sqlc.narg(chat_model_id)::uuid, bots.chat_model_id),
@@ -27,11 +29,12 @@ WITH updated AS (
search_provider_id = COALESCE(sqlc.narg(search_provider_id)::uuid, bots.search_provider_id),
updated_at = now()
WHERE bots.id = sqlc.arg(id)
RETURNING bots.id, bots.max_context_load_time, bots.language, bots.allow_guest, bots.chat_model_id, bots.memory_model_id, bots.embedding_model_id, bots.search_provider_id
RETURNING bots.id, bots.max_context_load_time, bots.max_context_tokens, bots.language, bots.allow_guest, bots.chat_model_id, bots.memory_model_id, bots.embedding_model_id, bots.search_provider_id
)
SELECT
updated.id AS bot_id,
updated.max_context_load_time,
updated.max_context_tokens,
updated.language,
updated.allow_guest,
chat_models.model_id AS chat_model_id,
@@ -47,6 +50,7 @@ LEFT JOIN search_providers ON search_providers.id = updated.search_provider_id;
-- name: DeleteSettingsByBotID :exec
UPDATE bots
SET max_context_load_time = 1440,
max_context_tokens = 0,
language = 'auto',
allow_guest = false,
chat_model_id = NULL,
+108 -22
View File
@@ -150,6 +150,12 @@ type gatewayRequest struct {
type gatewayResponse struct {
Messages []conversation.ModelMessage `json:"messages"`
Skills []string `json:"skills"`
Usage json.RawMessage `json:"usage,omitempty"`
}
type gatewayUsage struct {
InputTokens *int `json:"inputTokens"`
OutputTokens *int `json:"outputTokens"`
}
// gatewaySchedule matches the agent gateway ScheduleModel for /chat/trigger-schedule.
@@ -212,13 +218,15 @@ func (r *Resolver) resolve(ctx context.Context, req conversation.ChatRequest) (r
return resolvedContext{}, err
}
maxCtx := coalescePositiveInt(req.MaxContextLoadTime, botSettings.MaxContextLoadTime, defaultMaxContextMinutes)
maxTokens := botSettings.MaxContextTokens
var messages []conversation.ModelMessage
if !skipHistory && r.conversationSvc != nil {
messages, err = r.loadMessages(ctx, req.ChatID, maxCtx)
if err != nil {
return resolvedContext{}, err
loaded, loadErr := r.loadMessages(ctx, req.ChatID, maxCtx)
if loadErr != nil {
return resolvedContext{}, loadErr
}
messages = trimMessagesByTokens(loaded, maxTokens)
}
if memoryMsg := r.loadMemoryContextMessage(ctx, req); memoryMsg != nil {
messages = append(messages, *memoryMsg)
@@ -291,7 +299,7 @@ func (r *Resolver) Chat(ctx context.Context, req conversation.ChatRequest) (conv
if err != nil {
return conversation.ChatResponse{}, err
}
if err := r.storeRound(ctx, req, resp.Messages); err != nil {
if err := r.storeRound(ctx, req, resp.Messages, resp.Usage); err != nil {
return conversation.ChatResponse{}, err
}
return conversation.ChatResponse{
@@ -345,7 +353,7 @@ func (r *Resolver) TriggerSchedule(ctx context.Context, botID string, payload sc
if err != nil {
return err
}
return r.storeRound(ctx, req, resp.Messages)
return r.storeRound(ctx, req, resp.Messages, resp.Usage)
}
// --- StreamChat ---
@@ -552,7 +560,7 @@ func (r *Resolver) tryStoreStream(ctx context.Context, req conversation.ChatRequ
if eventType == "done" {
var resp gatewayResponse
if err := json.Unmarshal([]byte(data), &resp); err == nil && len(resp.Messages) > 0 {
return true, r.storeRound(ctx, req, resp.Messages)
return true, r.storeRound(ctx, req, resp.Messages, resp.Usage)
}
}
@@ -562,15 +570,16 @@ func (r *Resolver) tryStoreStream(ctx context.Context, req conversation.ChatRequ
Data json.RawMessage `json:"data"`
Messages []conversation.ModelMessage `json:"messages"`
Skills []string `json:"skills"`
Usage json.RawMessage `json:"usage,omitempty"`
}
if err := json.Unmarshal([]byte(data), &envelope); err == nil {
if (envelope.Type == "agent_end" || envelope.Type == "done") && len(envelope.Messages) > 0 {
return true, r.storeRound(ctx, req, envelope.Messages)
return true, r.storeRound(ctx, req, envelope.Messages, envelope.Usage)
}
if envelope.Type == "done" && len(envelope.Data) > 0 {
var resp gatewayResponse
if err := json.Unmarshal(envelope.Data, &resp); err == nil && len(resp.Messages) > 0 {
return true, r.storeRound(ctx, req, resp.Messages)
return true, r.storeRound(ctx, req, resp.Messages, resp.Usage)
}
}
}
@@ -578,7 +587,7 @@ func (r *Resolver) tryStoreStream(ctx context.Context, req conversation.ChatRequ
// fallback: data: {messages: [...]}
var resp gatewayResponse
if err := json.Unmarshal([]byte(data), &resp); err == nil && len(resp.Messages) > 0 {
return true, r.storeRound(ctx, req, resp.Messages)
return true, r.storeRound(ctx, req, resp.Messages, resp.Usage)
}
return false, nil
}
@@ -648,7 +657,12 @@ func (r *Resolver) resolveContainerID(ctx context.Context, botID, explicit strin
// --- message loading ---
func (r *Resolver) loadMessages(ctx context.Context, chatID string, maxContextMinutes int) ([]conversation.ModelMessage, error) {
type messageWithUsage struct {
Message conversation.ModelMessage
UsageInputTokens *int
}
func (r *Resolver) loadMessages(ctx context.Context, chatID string, maxContextMinutes int) ([]messageWithUsage, error) {
if r.messageService == nil {
return nil, nil
}
@@ -657,7 +671,7 @@ func (r *Resolver) loadMessages(ctx context.Context, chatID string, maxContextMi
if err != nil {
return nil, err
}
var result []conversation.ModelMessage
var result []messageWithUsage
for _, m := range msgs {
var mm conversation.ModelMessage
if err := json.Unmarshal(m.Content, &mm); err != nil {
@@ -667,11 +681,84 @@ func (r *Resolver) loadMessages(ctx context.Context, chatID string, maxContextMi
} else {
mm.Role = m.Role
}
result = append(result, mm)
var inputTokens *int
if len(m.Usage) > 0 {
var u gatewayUsage
if json.Unmarshal(m.Usage, &u) == nil {
inputTokens = u.InputTokens
}
}
result = append(result, messageWithUsage{Message: mm, UsageInputTokens: inputTokens})
}
return result, nil
}
func estimateMessageTokens(msg conversation.ModelMessage) int {
text := msg.TextContent()
if len(text) == 0 {
data, _ := json.Marshal(msg.Content)
return len(data) / 4
}
return len(text) / 4
}
func trimMessagesByTokens(messages []messageWithUsage, maxTokens int) []conversation.ModelMessage {
if maxTokens <= 0 || len(messages) == 0 {
result := make([]conversation.ModelMessage, len(messages))
for i, m := range messages {
result[i] = m.Message
}
return result
}
// Scan backwards. When a message with UsageInputTokens is found, that value
// represents the cumulative input tokens for all messages up to and including
// that message. Messages after it are estimated with chars/4.
totalTokens := 0
anchorFound := false
cutoff := 0
tailEstimate := 0
for i := len(messages) - 1; i >= 0; i-- {
if !anchorFound && messages[i].UsageInputTokens != nil {
anchorFound = true
totalTokens = *messages[i].UsageInputTokens + tailEstimate
if totalTokens > maxTokens {
cutoff = i + 1
break
}
continue
}
est := estimateMessageTokens(messages[i].Message)
if anchorFound {
totalTokens += est
if totalTokens > maxTokens {
cutoff = i + 1
break
}
} else {
tailEstimate += est
}
}
if !anchorFound {
totalTokens = 0
for i := len(messages) - 1; i >= 0; i-- {
totalTokens += estimateMessageTokens(messages[i].Message)
if totalTokens > maxTokens {
cutoff = i + 1
break
}
}
}
result := make([]conversation.ModelMessage, 0, len(messages)-cutoff)
for _, m := range messages[cutoff:] {
result = append(result, m.Message)
}
return result
}
type memoryContextItem struct {
Namespace string
Item memory.MemoryItem
@@ -792,9 +879,7 @@ func (r *Resolver) persistUserMessage(ctx context.Context, req conversation.Chat
return err
}
func (r *Resolver) storeRound(ctx context.Context, req conversation.ChatRequest, messages []conversation.ModelMessage) error {
// Add user query as the first message if not already present in the round.
// This ensures the user's prompt is persisted alongside the assistant's response.
func (r *Resolver) storeRound(ctx context.Context, req conversation.ChatRequest, messages []conversation.ModelMessage, usage json.RawMessage) error {
fullRound := make([]conversation.ModelMessage, 0, len(messages)+1)
hasUserQuery := false
for _, m := range messages {
@@ -813,7 +898,6 @@ func (r *Resolver) storeRound(ctx context.Context, req conversation.ChatRequest,
}
for _, m := range messages {
if req.UserMessagePersisted && m.Role == "user" && strings.TrimSpace(m.TextContent()) == strings.TrimSpace(req.Query) {
// User message was already persisted before streaming; skip duplicate copy in round payload.
continue
}
fullRound = append(fullRound, m)
@@ -822,14 +906,12 @@ func (r *Resolver) storeRound(ctx context.Context, req conversation.ChatRequest,
return nil
}
r.storeMessages(ctx, req, fullRound)
// Run memory extraction in the background so that the SSE stream can
// finish immediately after messages are persisted.
r.storeMessages(ctx, req, fullRound, usage)
go r.storeMemory(context.WithoutCancel(ctx), req.BotID, fullRound)
return nil
}
func (r *Resolver) storeMessages(ctx context.Context, req conversation.ChatRequest, messages []conversation.ModelMessage) {
func (r *Resolver) storeMessages(ctx context.Context, req conversation.ChatRequest, messages []conversation.ModelMessage, usage json.RawMessage) {
if r.messageService == nil {
return
}
@@ -838,7 +920,7 @@ func (r *Resolver) storeMessages(ctx context.Context, req conversation.ChatReque
}
meta := buildRouteMetadata(req)
senderChannelIdentityID, senderUserID := r.resolvePersistSenderIDs(ctx, req)
for _, msg := range messages {
for i, msg := range messages {
content, err := json.Marshal(msg)
if err != nil {
r.logger.Warn("storeMessages: marshal failed", slog.Any("error", err))
@@ -857,9 +939,12 @@ func (r *Resolver) storeMessages(ctx context.Context, req conversation.ChatReque
assets = chatAttachmentsToAssetRefs(req.Attachments)
}
} else if strings.TrimSpace(req.ExternalMessageID) != "" {
// Assistant/tool/system outputs are linked to the inbound source message for cross-channel reply threading.
sourceReplyToMessageID = req.ExternalMessageID
}
var msgUsage json.RawMessage
if i == len(messages)-1 && len(usage) > 0 {
msgUsage = usage
}
if _, err := r.messageService.Persist(ctx, messagepkg.PersistInput{
BotID: req.BotID,
RouteID: req.RouteID,
@@ -871,6 +956,7 @@ func (r *Resolver) storeMessages(ctx context.Context, req conversation.ChatReque
Role: msg.Role,
Content: content,
Metadata: meta,
Usage: msgUsage,
Assets: assets,
}); err != nil {
r.logger.Warn("persist message failed", slog.Any("error", err))
+12 -6
View File
@@ -14,7 +14,7 @@ import (
const createBot = `-- name: CreateBot :one
INSERT INTO bots (owner_user_id, type, display_name, avatar_url, is_active, metadata, status)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
`
type CreateBotParams struct {
@@ -47,6 +47,7 @@ func (q *Queries) CreateBot(ctx context.Context, arg CreateBotParams) (Bot, erro
&i.IsActive,
&i.Status,
&i.MaxContextLoadTime,
&i.MaxContextTokens,
&i.Language,
&i.AllowGuest,
&i.ChatModelID,
@@ -84,7 +85,7 @@ func (q *Queries) DeleteBotMember(ctx context.Context, arg DeleteBotMemberParams
}
const getBotByID = `-- name: GetBotByID :one
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
FROM bots
WHERE id = $1
`
@@ -101,6 +102,7 @@ func (q *Queries) GetBotByID(ctx context.Context, id pgtype.UUID) (Bot, error) {
&i.IsActive,
&i.Status,
&i.MaxContextLoadTime,
&i.MaxContextTokens,
&i.Language,
&i.AllowGuest,
&i.ChatModelID,
@@ -171,7 +173,7 @@ func (q *Queries) ListBotMembers(ctx context.Context, botID pgtype.UUID) ([]BotM
}
const listBotsByMember = `-- name: ListBotsByMember :many
SELECT b.id, b.owner_user_id, b.type, b.display_name, b.avatar_url, b.is_active, b.status, b.max_context_load_time, b.language, b.allow_guest, b.chat_model_id, b.memory_model_id, b.embedding_model_id, b.search_provider_id, b.metadata, b.created_at, b.updated_at
SELECT b.id, b.owner_user_id, b.type, b.display_name, b.avatar_url, b.is_active, b.status, b.max_context_load_time, b.max_context_tokens, b.language, b.allow_guest, b.chat_model_id, b.memory_model_id, b.embedding_model_id, b.search_provider_id, b.metadata, b.created_at, b.updated_at
FROM bots b
JOIN bot_members m ON m.bot_id = b.id
WHERE m.user_id = $1
@@ -196,6 +198,7 @@ func (q *Queries) ListBotsByMember(ctx context.Context, userID pgtype.UUID) ([]B
&i.IsActive,
&i.Status,
&i.MaxContextLoadTime,
&i.MaxContextTokens,
&i.Language,
&i.AllowGuest,
&i.ChatModelID,
@@ -217,7 +220,7 @@ func (q *Queries) ListBotsByMember(ctx context.Context, userID pgtype.UUID) ([]B
}
const listBotsByOwner = `-- name: ListBotsByOwner :many
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
SELECT id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
FROM bots
WHERE owner_user_id = $1
ORDER BY created_at DESC
@@ -241,6 +244,7 @@ func (q *Queries) ListBotsByOwner(ctx context.Context, ownerUserID pgtype.UUID)
&i.IsActive,
&i.Status,
&i.MaxContextLoadTime,
&i.MaxContextTokens,
&i.Language,
&i.AllowGuest,
&i.ChatModelID,
@@ -266,7 +270,7 @@ UPDATE bots
SET owner_user_id = $2,
updated_at = now()
WHERE id = $1
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
`
type UpdateBotOwnerParams struct {
@@ -286,6 +290,7 @@ func (q *Queries) UpdateBotOwner(ctx context.Context, arg UpdateBotOwnerParams)
&i.IsActive,
&i.Status,
&i.MaxContextLoadTime,
&i.MaxContextTokens,
&i.Language,
&i.AllowGuest,
&i.ChatModelID,
@@ -307,7 +312,7 @@ SET display_name = $2,
metadata = $5,
updated_at = now()
WHERE id = $1
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
`
type UpdateBotProfileParams struct {
@@ -336,6 +341,7 @@ func (q *Queries) UpdateBotProfile(ctx context.Context, arg UpdateBotProfilePara
&i.IsActive,
&i.Status,
&i.MaxContextLoadTime,
&i.MaxContextTokens,
&i.Language,
&i.AllowGuest,
&i.ChatModelID,
+1 -1
View File
@@ -590,7 +590,7 @@ WITH updated AS (
SET display_name = $1,
updated_at = now()
WHERE bots.id = $2
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
RETURNING id, owner_user_id, type, display_name, avatar_url, is_active, status, max_context_load_time, max_context_tokens, language, allow_guest, chat_model_id, memory_model_id, embedding_model_id, search_provider_id, metadata, created_at, updated_at
)
SELECT
updated.id AS id,
+21 -2
View File
@@ -22,7 +22,8 @@ INSERT INTO bot_history_messages (
source_reply_to_message_id,
role,
content,
metadata
metadata,
usage
)
VALUES (
$1,
@@ -34,7 +35,8 @@ VALUES (
$7::text,
$8,
$9,
$10
$10,
$11
)
RETURNING
id,
@@ -48,6 +50,7 @@ RETURNING
role,
content,
metadata,
usage,
created_at
`
@@ -62,6 +65,7 @@ type CreateMessageParams struct {
Role string `json:"role"`
Content []byte `json:"content"`
Metadata []byte `json:"metadata"`
Usage []byte `json:"usage"`
}
type CreateMessageRow struct {
@@ -76,6 +80,7 @@ type CreateMessageRow struct {
Role string `json:"role"`
Content []byte `json:"content"`
Metadata []byte `json:"metadata"`
Usage []byte `json:"usage"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
@@ -91,6 +96,7 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (C
arg.Role,
arg.Content,
arg.Metadata,
arg.Usage,
)
var i CreateMessageRow
err := row.Scan(
@@ -105,6 +111,7 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (C
&i.Role,
&i.Content,
&i.Metadata,
&i.Usage,
&i.CreatedAt,
)
return i, err
@@ -133,6 +140,7 @@ SELECT
m.role,
m.content,
m.metadata,
m.usage,
m.created_at,
ci.display_name AS sender_display_name,
ci.avatar_url AS sender_avatar_url
@@ -155,6 +163,7 @@ type ListMessagesRow struct {
Role string `json:"role"`
Content []byte `json:"content"`
Metadata []byte `json:"metadata"`
Usage []byte `json:"usage"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
SenderDisplayName pgtype.Text `json:"sender_display_name"`
SenderAvatarUrl pgtype.Text `json:"sender_avatar_url"`
@@ -181,6 +190,7 @@ func (q *Queries) ListMessages(ctx context.Context, botID pgtype.UUID) ([]ListMe
&i.Role,
&i.Content,
&i.Metadata,
&i.Usage,
&i.CreatedAt,
&i.SenderDisplayName,
&i.SenderAvatarUrl,
@@ -208,6 +218,7 @@ SELECT
m.role,
m.content,
m.metadata,
m.usage,
m.created_at,
ci.display_name AS sender_display_name,
ci.avatar_url AS sender_avatar_url
@@ -237,6 +248,7 @@ type ListMessagesBeforeRow struct {
Role string `json:"role"`
Content []byte `json:"content"`
Metadata []byte `json:"metadata"`
Usage []byte `json:"usage"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
SenderDisplayName pgtype.Text `json:"sender_display_name"`
SenderAvatarUrl pgtype.Text `json:"sender_avatar_url"`
@@ -263,6 +275,7 @@ func (q *Queries) ListMessagesBefore(ctx context.Context, arg ListMessagesBefore
&i.Role,
&i.Content,
&i.Metadata,
&i.Usage,
&i.CreatedAt,
&i.SenderDisplayName,
&i.SenderAvatarUrl,
@@ -290,6 +303,7 @@ SELECT
m.role,
m.content,
m.metadata,
m.usage,
m.created_at,
ci.display_name AS sender_display_name,
ci.avatar_url AS sender_avatar_url
@@ -317,6 +331,7 @@ type ListMessagesLatestRow struct {
Role string `json:"role"`
Content []byte `json:"content"`
Metadata []byte `json:"metadata"`
Usage []byte `json:"usage"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
SenderDisplayName pgtype.Text `json:"sender_display_name"`
SenderAvatarUrl pgtype.Text `json:"sender_avatar_url"`
@@ -343,6 +358,7 @@ func (q *Queries) ListMessagesLatest(ctx context.Context, arg ListMessagesLatest
&i.Role,
&i.Content,
&i.Metadata,
&i.Usage,
&i.CreatedAt,
&i.SenderDisplayName,
&i.SenderAvatarUrl,
@@ -370,6 +386,7 @@ SELECT
m.role,
m.content,
m.metadata,
m.usage,
m.created_at,
ci.display_name AS sender_display_name,
ci.avatar_url AS sender_avatar_url
@@ -397,6 +414,7 @@ type ListMessagesSinceRow struct {
Role string `json:"role"`
Content []byte `json:"content"`
Metadata []byte `json:"metadata"`
Usage []byte `json:"usage"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
SenderDisplayName pgtype.Text `json:"sender_display_name"`
SenderAvatarUrl pgtype.Text `json:"sender_avatar_url"`
@@ -423,6 +441,7 @@ func (q *Queries) ListMessagesSince(ctx context.Context, arg ListMessagesSincePa
&i.Role,
&i.Content,
&i.Metadata,
&i.Usage,
&i.CreatedAt,
&i.SenderDisplayName,
&i.SenderAvatarUrl,
+2
View File
@@ -17,6 +17,7 @@ type Bot struct {
IsActive bool `json:"is_active"`
Status string `json:"status"`
MaxContextLoadTime int32 `json:"max_context_load_time"`
MaxContextTokens int32 `json:"max_context_tokens"`
Language string `json:"language"`
AllowGuest bool `json:"allow_guest"`
ChatModelID pgtype.UUID `json:"chat_model_id"`
@@ -69,6 +70,7 @@ type BotHistoryMessage struct {
Role string `json:"role"`
Content []byte `json:"content"`
Metadata []byte `json:"metadata"`
Usage []byte `json:"usage"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
+18 -8
View File
@@ -14,6 +14,7 @@ import (
const deleteSettingsByBotID = `-- name: DeleteSettingsByBotID :exec
UPDATE bots
SET max_context_load_time = 1440,
max_context_tokens = 0,
language = 'auto',
allow_guest = false,
chat_model_id = NULL,
@@ -33,6 +34,7 @@ const getSettingsByBotID = `-- name: GetSettingsByBotID :one
SELECT
bots.id AS bot_id,
bots.max_context_load_time,
bots.max_context_tokens,
bots.language,
bots.allow_guest,
chat_models.model_id AS chat_model_id,
@@ -50,6 +52,7 @@ WHERE bots.id = $1
type GetSettingsByBotIDRow struct {
BotID pgtype.UUID `json:"bot_id"`
MaxContextLoadTime int32 `json:"max_context_load_time"`
MaxContextTokens int32 `json:"max_context_tokens"`
Language string `json:"language"`
AllowGuest bool `json:"allow_guest"`
ChatModelID pgtype.Text `json:"chat_model_id"`
@@ -64,6 +67,7 @@ func (q *Queries) GetSettingsByBotID(ctx context.Context, id pgtype.UUID) (GetSe
err := row.Scan(
&i.BotID,
&i.MaxContextLoadTime,
&i.MaxContextTokens,
&i.Language,
&i.AllowGuest,
&i.ChatModelID,
@@ -78,19 +82,21 @@ const upsertBotSettings = `-- name: UpsertBotSettings :one
WITH updated AS (
UPDATE bots
SET max_context_load_time = $1,
language = $2,
allow_guest = $3,
chat_model_id = COALESCE($4::uuid, bots.chat_model_id),
memory_model_id = COALESCE($5::uuid, bots.memory_model_id),
embedding_model_id = COALESCE($6::uuid, bots.embedding_model_id),
search_provider_id = COALESCE($7::uuid, bots.search_provider_id),
max_context_tokens = $2,
language = $3,
allow_guest = $4,
chat_model_id = COALESCE($5::uuid, bots.chat_model_id),
memory_model_id = COALESCE($6::uuid, bots.memory_model_id),
embedding_model_id = COALESCE($7::uuid, bots.embedding_model_id),
search_provider_id = COALESCE($8::uuid, bots.search_provider_id),
updated_at = now()
WHERE bots.id = $8
RETURNING bots.id, bots.max_context_load_time, bots.language, bots.allow_guest, bots.chat_model_id, bots.memory_model_id, bots.embedding_model_id, bots.search_provider_id
WHERE bots.id = $9
RETURNING bots.id, bots.max_context_load_time, bots.max_context_tokens, bots.language, bots.allow_guest, bots.chat_model_id, bots.memory_model_id, bots.embedding_model_id, bots.search_provider_id
)
SELECT
updated.id AS bot_id,
updated.max_context_load_time,
updated.max_context_tokens,
updated.language,
updated.allow_guest,
chat_models.model_id AS chat_model_id,
@@ -106,6 +112,7 @@ LEFT JOIN search_providers ON search_providers.id = updated.search_provider_id
type UpsertBotSettingsParams struct {
MaxContextLoadTime int32 `json:"max_context_load_time"`
MaxContextTokens int32 `json:"max_context_tokens"`
Language string `json:"language"`
AllowGuest bool `json:"allow_guest"`
ChatModelID pgtype.UUID `json:"chat_model_id"`
@@ -118,6 +125,7 @@ type UpsertBotSettingsParams struct {
type UpsertBotSettingsRow struct {
BotID pgtype.UUID `json:"bot_id"`
MaxContextLoadTime int32 `json:"max_context_load_time"`
MaxContextTokens int32 `json:"max_context_tokens"`
Language string `json:"language"`
AllowGuest bool `json:"allow_guest"`
ChatModelID pgtype.Text `json:"chat_model_id"`
@@ -129,6 +137,7 @@ type UpsertBotSettingsRow struct {
func (q *Queries) UpsertBotSettings(ctx context.Context, arg UpsertBotSettingsParams) (UpsertBotSettingsRow, error) {
row := q.db.QueryRow(ctx, upsertBotSettings,
arg.MaxContextLoadTime,
arg.MaxContextTokens,
arg.Language,
arg.AllowGuest,
arg.ChatModelID,
@@ -141,6 +150,7 @@ func (q *Queries) UpsertBotSettings(ctx context.Context, arg UpsertBotSettingsPa
err := row.Scan(
&i.BotID,
&i.MaxContextLoadTime,
&i.MaxContextTokens,
&i.Language,
&i.AllowGuest,
&i.ChatModelID,
+8
View File
@@ -79,6 +79,7 @@ func (s *DBService) Persist(ctx context.Context, input PersistInput) (Message, e
Role: input.Role,
Content: content,
Metadata: metaBytes,
Usage: input.Usage,
})
if err != nil {
return Message{}, err
@@ -213,6 +214,7 @@ func toMessageFromCreate(row sqlc.CreateMessageRow) Message {
row.Role,
row.Content,
row.Metadata,
row.Usage,
row.CreatedAt,
)
}
@@ -232,6 +234,7 @@ func toMessageFromListRow(row sqlc.ListMessagesRow) Message {
row.Role,
row.Content,
row.Metadata,
row.Usage,
row.CreatedAt,
)
}
@@ -251,6 +254,7 @@ func toMessageFromSinceRow(row sqlc.ListMessagesSinceRow) Message {
row.Role,
row.Content,
row.Metadata,
row.Usage,
row.CreatedAt,
)
}
@@ -270,6 +274,7 @@ func toMessageFromLatestRow(row sqlc.ListMessagesLatestRow) Message {
row.Role,
row.Content,
row.Metadata,
row.Usage,
row.CreatedAt,
)
}
@@ -288,6 +293,7 @@ func toMessageFields(
role string,
content []byte,
metadata []byte,
usage []byte,
createdAt pgtype.Timestamptz,
) Message {
return Message{
@@ -304,6 +310,7 @@ func toMessageFields(
Role: role,
Content: json.RawMessage(content),
Metadata: parseJSONMap(metadata),
Usage: json.RawMessage(usage),
CreatedAt: createdAt.Time,
}
}
@@ -347,6 +354,7 @@ func toMessageFromBeforeRow(row sqlc.ListMessagesBeforeRow) Message {
row.Role,
row.Content,
row.Metadata,
row.Usage,
row.CreatedAt,
)
}
+2
View File
@@ -36,6 +36,7 @@ type Message struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
Metadata map[string]any `json:"metadata,omitempty"`
Usage json.RawMessage `json:"usage,omitempty"`
Assets []MessageAsset `json:"assets,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
@@ -59,6 +60,7 @@ type PersistInput struct {
Role string
Content json.RawMessage
Metadata map[string]any
Usage json.RawMessage
Assets []AssetRef
}
+14 -3
View File
@@ -54,10 +54,13 @@ func (s *Service) UpsertBot(ctx context.Context, botID string, req UpsertRequest
}
isPersonalBot := strings.EqualFold(strings.TrimSpace(botRow.Type), "personal")
current := normalizeBotSetting(botRow.MaxContextLoadTime, botRow.Language, botRow.AllowGuest)
current := normalizeBotSetting(botRow.MaxContextLoadTime, botRow.MaxContextTokens, botRow.Language, botRow.AllowGuest)
if req.MaxContextLoadTime != nil && *req.MaxContextLoadTime > 0 {
current.MaxContextLoadTime = *req.MaxContextLoadTime
}
if req.MaxContextTokens != nil && *req.MaxContextTokens >= 0 {
current.MaxContextTokens = *req.MaxContextTokens
}
if strings.TrimSpace(req.Language) != "" {
current.Language = strings.TrimSpace(req.Language)
}
@@ -106,6 +109,7 @@ func (s *Service) UpsertBot(ctx context.Context, botID string, req UpsertRequest
updated, err := s.queries.UpsertBotSettings(ctx, sqlc.UpsertBotSettingsParams{
ID: pgID,
MaxContextLoadTime: int32(current.MaxContextLoadTime),
MaxContextTokens: int32(current.MaxContextTokens),
Language: current.Language,
AllowGuest: current.AllowGuest,
ChatModelID: chatModelUUID,
@@ -130,15 +134,19 @@ func (s *Service) Delete(ctx context.Context, botID string) error {
return s.queries.DeleteSettingsByBotID(ctx, pgID)
}
func normalizeBotSetting(maxContextLoadTime int32, language string, allowGuest bool) Settings {
func normalizeBotSetting(maxContextLoadTime int32, maxContextTokens int32, language string, allowGuest bool) Settings {
settings := Settings{
MaxContextLoadTime: int(maxContextLoadTime),
MaxContextTokens: int(maxContextTokens),
Language: strings.TrimSpace(language),
AllowGuest: allowGuest,
}
if settings.MaxContextLoadTime <= 0 {
settings.MaxContextLoadTime = DefaultMaxContextLoadTime
}
if settings.MaxContextTokens < 0 {
settings.MaxContextTokens = 0
}
if settings.Language == "" {
settings.Language = DefaultLanguage
}
@@ -148,6 +156,7 @@ func normalizeBotSetting(maxContextLoadTime int32, language string, allowGuest b
func normalizeBotSettingsReadRow(row sqlc.GetSettingsByBotIDRow) Settings {
return normalizeBotSettingsFields(
row.MaxContextLoadTime,
row.MaxContextTokens,
row.Language,
row.AllowGuest,
row.ChatModelID,
@@ -160,6 +169,7 @@ func normalizeBotSettingsReadRow(row sqlc.GetSettingsByBotIDRow) Settings {
func normalizeBotSettingsWriteRow(row sqlc.UpsertBotSettingsRow) Settings {
return normalizeBotSettingsFields(
row.MaxContextLoadTime,
row.MaxContextTokens,
row.Language,
row.AllowGuest,
row.ChatModelID,
@@ -171,6 +181,7 @@ func normalizeBotSettingsWriteRow(row sqlc.UpsertBotSettingsRow) Settings {
func normalizeBotSettingsFields(
maxContextLoadTime int32,
maxContextTokens int32,
language string,
allowGuest bool,
chatModelID pgtype.Text,
@@ -178,7 +189,7 @@ func normalizeBotSettingsFields(
embeddingModelID pgtype.Text,
searchProviderID pgtype.UUID,
) Settings {
settings := normalizeBotSetting(maxContextLoadTime, language, allowGuest)
settings := normalizeBotSetting(maxContextLoadTime, maxContextTokens, language, allowGuest)
settings.ChatModelID = strings.TrimSpace(chatModelID.String)
settings.MemoryModelID = strings.TrimSpace(memoryModelID.String)
settings.EmbeddingModelID = strings.TrimSpace(embeddingModelID.String)
+2
View File
@@ -11,6 +11,7 @@ type Settings struct {
EmbeddingModelID string `json:"embedding_model_id"`
SearchProviderID string `json:"search_provider_id"`
MaxContextLoadTime int `json:"max_context_load_time"`
MaxContextTokens int `json:"max_context_tokens"`
Language string `json:"language"`
AllowGuest bool `json:"allow_guest"`
}
@@ -21,6 +22,7 @@ type UpsertRequest struct {
EmbeddingModelID string `json:"embedding_model_id,omitempty"`
SearchProviderID string `json:"search_provider_id,omitempty"`
MaxContextLoadTime *int `json:"max_context_load_time,omitempty"`
MaxContextTokens *int `json:"max_context_tokens,omitempty"`
Language string `json:"language,omitempty"`
AllowGuest *bool `json:"allow_guest,omitempty"`
}
+3
View File
@@ -673,6 +673,7 @@ export type MessageMessage = {
sender_display_name?: string;
sender_user_id?: string;
source_reply_to_message_id?: string;
usage?: Array<number>;
};
export type MessageMessageAsset = {
@@ -866,6 +867,7 @@ export type SettingsSettings = {
embedding_model_id?: string;
language?: string;
max_context_load_time?: number;
max_context_tokens?: number;
memory_model_id?: string;
search_provider_id?: string;
};
@@ -876,6 +878,7 @@ export type SettingsUpsertRequest = {
embedding_model_id?: string;
language?: string;
max_context_load_time?: number;
max_context_tokens?: number;
memory_model_id?: string;
search_provider_id?: string;
};
+1
View File
@@ -354,6 +354,7 @@
"searchProvider": "Search Provider",
"searchProviderPlaceholder": "Select search provider",
"maxContextLoadTime": "Max Context Load Time",
"maxContextTokens": "Max Context Tokens",
"language": "Language",
"allowGuest": "Allow Guest Access",
"allowGuestPersonalHint": "Personal bots do not support guest access. Use a public bot instead.",
+1
View File
@@ -350,6 +350,7 @@
"searchProvider": "搜索提供方",
"searchProviderPlaceholder": "选择搜索提供方",
"maxContextLoadTime": "最大上下文加载时间",
"maxContextTokens": "最大上下文Token数",
"language": "语言",
"allowGuest": "允许游客访问",
"allowGuestPersonalHint": "个人 Bot 不支持游客访问,请使用公开 Bot。",
@@ -58,6 +58,17 @@
/>
</div>
<!-- Max Context Tokens -->
<div class="space-y-2">
<Label>{{ $t('bots.settings.maxContextTokens') }}</Label>
<Input
v-model.number="form.max_context_tokens"
type="number"
:min="0"
placeholder="0"
/>
</div>
<!-- Language -->
<div class="space-y-2">
<Label>{{ $t('bots.settings.language') }}</Label>
@@ -226,6 +237,7 @@ const form = reactive<SettingsSettings>({
embedding_model_id: '',
search_provider_id: '',
max_context_load_time: 0,
max_context_tokens: 0,
language: '',
allow_guest: false,
})
@@ -238,6 +250,7 @@ watch(settings, (val) => {
form.embedding_model_id = val.embedding_model_id ?? ''
form.search_provider_id = val.search_provider_id ?? ''
form.max_context_load_time = val.max_context_load_time ?? 0
form.max_context_tokens = val.max_context_tokens ?? 0
form.language = val.language ?? ''
form.allow_guest = val.allow_guest ?? false
}
@@ -252,6 +265,7 @@ const hasChanges = computed(() => {
|| form.embedding_model_id !== (s.embedding_model_id ?? '')
|| form.search_provider_id !== (s.search_provider_id ?? '')
|| form.max_context_load_time !== (s.max_context_load_time ?? 0)
|| form.max_context_tokens !== (s.max_context_tokens ?? 0)
|| form.language !== (s.language ?? '')
if (isPublicBot.value) {
changed = changed || form.allow_guest !== (s.allow_guest ?? false)
+12
View File
@@ -6451,6 +6451,12 @@ const docTemplate = `{
},
"source_reply_to_message_id": {
"type": "string"
},
"usage": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
@@ -6949,6 +6955,9 @@ const docTemplate = `{
"max_context_load_time": {
"type": "integer"
},
"max_context_tokens": {
"type": "integer"
},
"memory_model_id": {
"type": "string"
},
@@ -6975,6 +6984,9 @@ const docTemplate = `{
"max_context_load_time": {
"type": "integer"
},
"max_context_tokens": {
"type": "integer"
},
"memory_model_id": {
"type": "string"
},
+12
View File
@@ -6442,6 +6442,12 @@
},
"source_reply_to_message_id": {
"type": "string"
},
"usage": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
@@ -6940,6 +6946,9 @@
"max_context_load_time": {
"type": "integer"
},
"max_context_tokens": {
"type": "integer"
},
"memory_model_id": {
"type": "string"
},
@@ -6966,6 +6975,9 @@
"max_context_load_time": {
"type": "integer"
},
"max_context_tokens": {
"type": "integer"
},
"memory_model_id": {
"type": "string"
},
+8
View File
@@ -1101,6 +1101,10 @@ definitions:
type: string
source_reply_to_message_id:
type: string
usage:
items:
type: integer
type: array
type: object
message.MessageAsset:
properties:
@@ -1435,6 +1439,8 @@ definitions:
type: string
max_context_load_time:
type: integer
max_context_tokens:
type: integer
memory_model_id:
type: string
search_provider_id:
@@ -1452,6 +1458,8 @@ definitions:
type: string
max_context_load_time:
type: integer
max_context_tokens:
type: integer
memory_model_id:
type: string
search_provider_id: