Files
Memoh/internal/handlers/session_info.go
T
Acbox 33b57ee345 feat: rename info to status, add /status slash command
Rename session info endpoint from /sessions/:id/info to /sessions/:id/status
and update frontend tab label accordingly. Add /status slash command that
displays current session metrics (message count, context usage, cache hit
rate, used skills) as formatted text in any channel.
2026-04-03 01:17:33 +08:00

178 lines
5.4 KiB
Go

package handlers
import (
"errors"
"log/slog"
"net/http"
"strings"
"github.com/jackc/pgx/v5"
"github.com/labstack/echo/v4"
"github.com/memohai/memoh/internal/accounts"
"github.com/memohai/memoh/internal/bots"
"github.com/memohai/memoh/internal/db"
"github.com/memohai/memoh/internal/db/sqlc"
"github.com/memohai/memoh/internal/models"
"github.com/memohai/memoh/internal/settings"
)
type SessionInfoHandler struct {
queries *sqlc.Queries
botService *bots.Service
accountService *accounts.Service
modelsService *models.Service
settingsService *settings.Service
logger *slog.Logger
}
func NewSessionInfoHandler(log *slog.Logger, queries *sqlc.Queries, botService *bots.Service, accountService *accounts.Service, modelsService *models.Service, settingsService *settings.Service) *SessionInfoHandler {
return &SessionInfoHandler{
queries: queries,
botService: botService,
accountService: accountService,
modelsService: modelsService,
settingsService: settingsService,
logger: log.With(slog.String("handler", "session_info")),
}
}
func (h *SessionInfoHandler) Register(e *echo.Echo) {
e.GET("/bots/:bot_id/sessions/:session_id/status", h.GetSessionInfo)
}
type SessionInfoResponse struct {
MessageCount int64 `json:"message_count"`
ContextUsage ContextUsage `json:"context_usage"`
CacheStats CacheStats `json:"cache_stats"`
Skills []string `json:"skills"`
}
type ContextUsage struct {
UsedTokens int64 `json:"used_tokens"`
ContextWindow *int64 `json:"context_window,omitempty"`
}
type CacheStats struct {
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheWriteTokens int64 `json:"cache_write_tokens"`
TotalInputTokens int64 `json:"total_input_tokens"`
CacheHitRate float64 `json:"cache_hit_rate"`
}
// GetSessionInfo godoc
// @Summary Get session info
// @Description Get aggregated info for a chat session including message count, context usage, cache stats, and used skills
// @Tags sessions
// @Param bot_id path string true "Bot ID"
// @Param session_id path string true "Session ID"
// @Param model_id query string false "Optional model UUID override for context window"
// @Success 200 {object} SessionInfoResponse
// @Failure 400 {object} ErrorResponse
// @Failure 403 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /bots/{bot_id}/sessions/{session_id}/status [get].
func (h *SessionInfoHandler) GetSessionInfo(c echo.Context) error {
userID, err := RequireChannelIdentityID(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 := AuthorizeBotAccess(c.Request().Context(), h.botService, h.accountService, userID, botID); err != nil {
return err
}
sessionID := strings.TrimSpace(c.Param("session_id"))
if sessionID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "session id is required")
}
pgSessionID, err := db.ParseUUID(sessionID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid session id")
}
ctx := c.Request().Context()
messageCount, err := h.queries.CountMessagesBySession(ctx, pgSessionID)
if err != nil {
h.logger.Error("count messages failed", slog.Any("error", err))
return echo.NewHTTPError(http.StatusInternalServerError, "failed to count messages")
}
var usedTokens int64
latestUsage, err := h.queries.GetLatestAssistantUsage(ctx, pgSessionID)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
h.logger.Error("get latest usage failed", slog.Any("error", err))
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get latest usage")
}
if err == nil {
usedTokens = latestUsage
}
contextWindow := h.resolveContextWindow(c, botID)
cacheRow, err := h.queries.GetSessionCacheStats(ctx, pgSessionID)
if err != nil {
h.logger.Error("get cache stats failed", slog.Any("error", err))
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get cache stats")
}
var cacheHitRate float64
if cacheRow.TotalInputTokens > 0 {
cacheHitRate = float64(cacheRow.CacheReadTokens) / float64(cacheRow.TotalInputTokens) * 100
}
skills, err := h.queries.GetSessionUsedSkills(ctx, pgSessionID)
if err != nil {
h.logger.Error("get used skills failed", slog.Any("error", err))
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get used skills")
}
if skills == nil {
skills = []string{}
}
resp := SessionInfoResponse{
MessageCount: messageCount,
ContextUsage: ContextUsage{
UsedTokens: usedTokens,
ContextWindow: contextWindow,
},
CacheStats: CacheStats{
CacheReadTokens: cacheRow.CacheReadTokens,
CacheWriteTokens: cacheRow.CacheWriteTokens,
TotalInputTokens: cacheRow.TotalInputTokens,
CacheHitRate: cacheHitRate,
},
Skills: skills,
}
return c.JSON(http.StatusOK, resp)
}
func (h *SessionInfoHandler) resolveContextWindow(c echo.Context, botID string) *int64 {
modelIDStr := strings.TrimSpace(c.QueryParam("model_id"))
if modelIDStr == "" && h.settingsService != nil {
s, err := h.settingsService.GetBot(c.Request().Context(), botID)
if err == nil && s.ChatModelID != "" {
modelIDStr = s.ChatModelID
}
}
if modelIDStr == "" || h.modelsService == nil {
return nil
}
m, err := h.modelsService.GetByID(c.Request().Context(), modelIDStr)
if err != nil {
return nil
}
if m.Config.ContextWindow == nil {
return nil
}
cw := int64(*m.Config.ContextWindow)
return &cw
}