mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
27d2b99301
- Add POST /bots/:bot_id/sessions/:session_id/compact endpoint for synchronous context compaction with fallback to chat model when no dedicated compaction model is configured - Add "Compact Now" button to session info panel in the web UI - Add /compact slash command for triggering compaction from chat - Regenerate OpenAPI spec and TypeScript SDK
224 lines
7.1 KiB
Go
224 lines
7.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
|
|
"github.com/memohai/memoh/internal/accounts"
|
|
"github.com/memohai/memoh/internal/bots"
|
|
"github.com/memohai/memoh/internal/compaction"
|
|
"github.com/memohai/memoh/internal/db/sqlc"
|
|
"github.com/memohai/memoh/internal/models"
|
|
"github.com/memohai/memoh/internal/providers"
|
|
"github.com/memohai/memoh/internal/settings"
|
|
)
|
|
|
|
type CompactionHandler struct {
|
|
service *compaction.Service
|
|
botService *bots.Service
|
|
accountService *accounts.Service
|
|
settingsService *settings.Service
|
|
modelsService *models.Service
|
|
queries *sqlc.Queries
|
|
providersService *providers.Service
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewCompactionHandler(
|
|
log *slog.Logger,
|
|
service *compaction.Service,
|
|
botService *bots.Service,
|
|
accountService *accounts.Service,
|
|
settingsService *settings.Service,
|
|
modelsService *models.Service,
|
|
queries *sqlc.Queries,
|
|
providersService *providers.Service,
|
|
) *CompactionHandler {
|
|
return &CompactionHandler{
|
|
service: service,
|
|
botService: botService,
|
|
accountService: accountService,
|
|
settingsService: settingsService,
|
|
modelsService: modelsService,
|
|
queries: queries,
|
|
providersService: providersService,
|
|
logger: log.With(slog.String("handler", "compaction")),
|
|
}
|
|
}
|
|
|
|
func (h *CompactionHandler) Register(e *echo.Echo) {
|
|
group := e.Group("/bots/:bot_id/compaction")
|
|
group.GET("/logs", h.ListLogs)
|
|
group.DELETE("/logs", h.DeleteLogs)
|
|
e.POST("/bots/:bot_id/sessions/:session_id/compact", h.TriggerCompact)
|
|
}
|
|
|
|
// ListLogs godoc
|
|
// @Summary List compaction logs
|
|
// @Description List compaction logs for a bot
|
|
// @Tags compaction
|
|
// @Param bot_id path string true "Bot ID"
|
|
// @Param limit query int false "Limit" default(50)
|
|
// @Param offset query int false "Offset" default(0)
|
|
// @Success 200 {object} compaction.ListLogsResponse
|
|
// @Failure 400 {object} ErrorResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /bots/{bot_id}/compaction/logs [get].
|
|
func (h *CompactionHandler) ListLogs(c echo.Context) error {
|
|
userID, err := h.requireUserID(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
botID := strings.TrimSpace(c.Param("bot_id"))
|
|
if botID == "" {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "bot id is required")
|
|
}
|
|
if _, err := h.authorizeBotAccess(c.Request().Context(), userID, botID); err != nil {
|
|
return err
|
|
}
|
|
|
|
limit, offset := parseOffsetLimit(c)
|
|
items, total, err := h.service.ListLogs(c.Request().Context(), botID, limit, offset)
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
return c.JSON(http.StatusOK, compaction.ListLogsResponse{Items: items, TotalCount: total})
|
|
}
|
|
|
|
// DeleteLogs godoc
|
|
// @Summary Delete compaction logs
|
|
// @Description Delete all compaction logs for a bot
|
|
// @Tags compaction
|
|
// @Param bot_id path string true "Bot ID"
|
|
// @Success 204 "No Content"
|
|
// @Failure 400 {object} ErrorResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /bots/{bot_id}/compaction/logs [delete].
|
|
func (h *CompactionHandler) DeleteLogs(c echo.Context) error {
|
|
userID, err := h.requireUserID(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
botID := strings.TrimSpace(c.Param("bot_id"))
|
|
if botID == "" {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "bot id is required")
|
|
}
|
|
if _, err := h.authorizeBotAccess(c.Request().Context(), userID, botID); err != nil {
|
|
return err
|
|
}
|
|
if err := h.service.DeleteLogs(c.Request().Context(), botID); err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
// TriggerCompactResponse is the API response for triggering compaction.
|
|
type TriggerCompactResponse struct {
|
|
Status string `json:"status"`
|
|
Summary string `json:"summary,omitempty"`
|
|
MessageCount int `json:"message_count"`
|
|
}
|
|
|
|
// TriggerCompact godoc
|
|
// @Summary Trigger immediate context compaction
|
|
// @Description Run context compaction synchronously for a session
|
|
// @Tags compaction
|
|
// @Param bot_id path string true "Bot ID"
|
|
// @Param session_id path string true "Session ID"
|
|
// @Success 200 {object} TriggerCompactResponse
|
|
// @Failure 400 {object} ErrorResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /bots/{bot_id}/sessions/{session_id}/compact [post].
|
|
func (h *CompactionHandler) TriggerCompact(c echo.Context) error {
|
|
userID, err := h.requireUserID(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
botID := strings.TrimSpace(c.Param("bot_id"))
|
|
if botID == "" {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "bot id is required")
|
|
}
|
|
if _, err := h.authorizeBotAccess(c.Request().Context(), userID, botID); err != nil {
|
|
return err
|
|
}
|
|
sessionID := strings.TrimSpace(c.Param("session_id"))
|
|
if sessionID == "" {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "session id is required")
|
|
}
|
|
|
|
cfg, err := h.buildTriggerConfig(c.Request().Context(), botID, sessionID)
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
|
}
|
|
|
|
if err := h.service.RunCompactionSync(c.Request().Context(), cfg); err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
logs, _, err := h.service.ListLogs(c.Request().Context(), botID, 1, 0)
|
|
if err != nil || len(logs) == 0 {
|
|
return c.JSON(http.StatusOK, TriggerCompactResponse{Status: "ok"})
|
|
}
|
|
latest := logs[0]
|
|
return c.JSON(http.StatusOK, TriggerCompactResponse{
|
|
Status: latest.Status,
|
|
Summary: latest.Summary,
|
|
MessageCount: latest.MessageCount,
|
|
})
|
|
}
|
|
|
|
func (h *CompactionHandler) buildTriggerConfig(ctx context.Context, botID, sessionID string) (compaction.TriggerConfig, error) {
|
|
botSettings, err := h.settingsService.GetBot(ctx, botID)
|
|
if err != nil {
|
|
return compaction.TriggerConfig{}, err
|
|
}
|
|
modelID := botSettings.CompactionModelID
|
|
if modelID == "" {
|
|
modelID = botSettings.ChatModelID
|
|
}
|
|
if modelID == "" {
|
|
return compaction.TriggerConfig{}, echo.NewHTTPError(http.StatusBadRequest, "no compaction or chat model configured")
|
|
}
|
|
|
|
compactModel, err := h.modelsService.GetByID(ctx, modelID)
|
|
if err != nil {
|
|
return compaction.TriggerConfig{}, err
|
|
}
|
|
compactProvider, err := models.FetchProviderByID(ctx, h.queries, compactModel.ProviderID)
|
|
if err != nil {
|
|
return compaction.TriggerConfig{}, err
|
|
}
|
|
creds, err := h.providersService.ResolveModelCredentials(ctx, compactProvider)
|
|
if err != nil {
|
|
return compaction.TriggerConfig{}, err
|
|
}
|
|
|
|
cfg := compaction.TriggerConfig{
|
|
BotID: botID,
|
|
SessionID: sessionID,
|
|
ModelID: compactModel.ModelID,
|
|
ClientType: compactProvider.ClientType,
|
|
APIKey: creds.APIKey,
|
|
CodexAccountID: creds.CodexAccountID,
|
|
BaseURL: providers.ProviderConfigString(compactProvider, "base_url"),
|
|
Ratio: 100,
|
|
TotalInputTokens: 1,
|
|
}
|
|
if compactModel.Config.ContextWindow != nil && *compactModel.Config.ContextWindow > 0 {
|
|
cfg.MaxCompactTokens = *compactModel.Config.ContextWindow * 90 / 100
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (*CompactionHandler) requireUserID(c echo.Context) (string, error) {
|
|
return RequireChannelIdentityID(c)
|
|
}
|
|
|
|
func (h *CompactionHandler) authorizeBotAccess(ctx context.Context, userID, botID string) (bots.Bot, error) {
|
|
return AuthorizeBotAccess(ctx, h.botService, h.accountService, userID, botID)
|
|
}
|