mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
33b57ee345
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.
178 lines
5.4 KiB
Go
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
|
|
}
|