mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat: add session info panel with message count, context usage, cache stats, and skills
Add GET /bots/:bot_id/sessions/:session_id/info API endpoint that returns per-session message count, latest input token usage with model context window, aggregated KV cache hit rate, and skills invoked via use_skill tool calls. Frontend Info tab in the right sidebar now displays this data in a compact key-value layout with a context usage progress bar and clickable skill links.
This commit is contained in:
@@ -202,7 +202,17 @@
|
||||
"scheduleDescription": "Description",
|
||||
"schedulePattern": "Cron Pattern",
|
||||
"scheduleMaxCalls": "Max Calls",
|
||||
"viewSchedule": "View Schedule"
|
||||
"viewSchedule": "View Schedule",
|
||||
"infoMessages": "Messages",
|
||||
"infoContextUsage": "Context Usage",
|
||||
"infoContextTokens": "{used} / {window}",
|
||||
"infoContextTokensNoWindow": "{used} / --",
|
||||
"infoCacheHitRate": "Cache Hit Rate",
|
||||
"infoCacheRead": "Cache Read",
|
||||
"infoCacheWrite": "Cache Write",
|
||||
"infoSkills": "Skills",
|
||||
"infoNoSkills": "No skills used in this session",
|
||||
"infoNoData": "No data available"
|
||||
},
|
||||
"models": {
|
||||
"title": "Models",
|
||||
|
||||
@@ -198,7 +198,17 @@
|
||||
"scheduleDescription": "描述",
|
||||
"schedulePattern": "Cron 表达式",
|
||||
"scheduleMaxCalls": "最大调用次数",
|
||||
"viewSchedule": "查看定时任务"
|
||||
"viewSchedule": "查看定时任务",
|
||||
"infoMessages": "消息数",
|
||||
"infoContextUsage": "上下文使用率",
|
||||
"infoContextTokens": "{used} / {window}",
|
||||
"infoContextTokensNoWindow": "{used} / --",
|
||||
"infoCacheHitRate": "Cache 命中率",
|
||||
"infoCacheRead": "Cache 读取",
|
||||
"infoCacheWrite": "Cache 写入",
|
||||
"infoSkills": "Skills",
|
||||
"infoNoSkills": "此会话未使用任何 Skill",
|
||||
"infoNoData": "暂无数据"
|
||||
},
|
||||
"models": {
|
||||
"title": "模型",
|
||||
|
||||
@@ -290,11 +290,12 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="activeRightTab === 'info'"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
class="absolute inset-0"
|
||||
>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Info
|
||||
</p>
|
||||
<SessionInfoPanel
|
||||
:visible="activeRightTab === 'info'"
|
||||
:override-model-id="overrideModelId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,6 +377,7 @@ import FileManager from '@/components/file-manager/index.vue'
|
||||
import TerminalComponent from '@/components/terminal/index.vue'
|
||||
import ModelOptions from '@/pages/bots/components/model-options.vue'
|
||||
import ReasoningEffortSelect from '@/pages/bots/components/reasoning-effort-select.vue'
|
||||
import SessionInfoPanel from './session-info-panel.vue'
|
||||
import { EFFORT_LABELS, EFFORT_OPACITY } from '@/pages/bots/components/reasoning-effort'
|
||||
import { useMediaGallery } from '../composables/useMediaGallery'
|
||||
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<ScrollArea class="h-full">
|
||||
<div class="px-4 py-3">
|
||||
<!-- No session -->
|
||||
<div
|
||||
v-if="!sessionId"
|
||||
class="flex items-center justify-center h-40"
|
||||
>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ $t('chat.infoNoData') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Key-value rows -->
|
||||
<div class="divide-y divide-border text-xs">
|
||||
<!-- Messages -->
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<span class="text-muted-foreground">{{ $t('chat.infoMessages') }}</span>
|
||||
<span class="font-medium text-foreground tabular-nums">{{ info?.message_count ?? '--' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Context Usage -->
|
||||
<div class="py-2 space-y-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-muted-foreground">{{ $t('chat.infoContextUsage') }}</span>
|
||||
<span class="font-medium text-foreground tabular-nums">
|
||||
<template v-if="contextWindow != null">
|
||||
{{ formatTokenCount(usedTokens) }} / {{ formatTokenCount(contextWindow) }}
|
||||
<span class="text-muted-foreground font-normal ml-1">({{ contextPercent.toFixed(1) }}%)</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ formatTokenCount(usedTokens) }} / --
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="contextWindow != null && contextWindow > 0"
|
||||
class="w-full h-1 rounded-full bg-accent overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-full transition-all"
|
||||
:class="contextBarColor"
|
||||
:style="{ width: `${Math.min(contextPercent, 100)}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cache Hit Rate -->
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<span class="text-muted-foreground">{{ $t('chat.infoCacheHitRate') }}</span>
|
||||
<span class="font-medium text-foreground tabular-nums">{{ cacheHitRate }}%</span>
|
||||
</div>
|
||||
|
||||
<!-- Cache Read -->
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<span class="text-muted-foreground">{{ $t('chat.infoCacheRead') }}</span>
|
||||
<span class="font-medium text-foreground tabular-nums">{{ formatTokenCount(info?.cache_stats?.cache_read_tokens ?? 0) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Cache Write -->
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<span class="text-muted-foreground">{{ $t('chat.infoCacheWrite') }}</span>
|
||||
<span class="font-medium text-foreground tabular-nums">{{ formatTokenCount(info?.cache_stats?.cache_write_tokens ?? 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skills -->
|
||||
<div class="mt-3">
|
||||
<p class="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
|
||||
{{ $t('chat.infoSkills') }}
|
||||
</p>
|
||||
<div
|
||||
v-if="!skills.length"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
{{ $t('chat.infoNoSkills') }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="space-y-0.5"
|
||||
>
|
||||
<button
|
||||
v-for="skill in skills"
|
||||
:key="skill"
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 w-full px-2 py-1 rounded-md text-xs text-foreground hover:bg-accent transition-colors text-left"
|
||||
@click="openSkillFile(skill)"
|
||||
>
|
||||
<Sparkles class="size-3 text-muted-foreground shrink-0" />
|
||||
<span class="truncate">{{ skill }}</span>
|
||||
<ExternalLink class="size-3 text-muted-foreground shrink-0 ml-auto" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useQuery } from '@pinia/colada'
|
||||
import { Sparkles, ExternalLink } from 'lucide-vue-next'
|
||||
import { ScrollArea } from '@memohai/ui'
|
||||
import { getBotsByBotIdSessionsBySessionIdInfo } from '@memohai/sdk'
|
||||
import type { HandlersSessionInfoResponse } from '@memohai/sdk'
|
||||
import { useChatStore } from '@/store/chat-list'
|
||||
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
overrideModelId?: string
|
||||
}>()
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { currentBotId, sessionId } = storeToRefs(chatStore)
|
||||
const openInFileManager = inject(openInFileManagerKey, undefined)
|
||||
|
||||
const { data: info } = useQuery({
|
||||
key: () => ['session-info', currentBotId.value ?? '', sessionId.value ?? '', props.overrideModelId ?? ''],
|
||||
query: async () => {
|
||||
const { data } = await getBotsByBotIdSessionsBySessionIdInfo({
|
||||
path: {
|
||||
bot_id: currentBotId.value!,
|
||||
session_id: sessionId.value!,
|
||||
},
|
||||
query: {
|
||||
model_id: props.overrideModelId || undefined,
|
||||
},
|
||||
throwOnError: true,
|
||||
})
|
||||
return data as HandlersSessionInfoResponse
|
||||
},
|
||||
enabled: () => !!currentBotId.value && !!sessionId.value && props.visible,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const usedTokens = computed(() => info.value?.context_usage?.used_tokens ?? 0)
|
||||
const contextWindow = computed(() => info.value?.context_usage?.context_window ?? null)
|
||||
const contextPercent = computed(() => {
|
||||
if (contextWindow.value == null || contextWindow.value <= 0) return 0
|
||||
return (usedTokens.value / contextWindow.value) * 100
|
||||
})
|
||||
const contextBarColor = computed(() => {
|
||||
if (contextPercent.value >= 90) return 'bg-destructive'
|
||||
if (contextPercent.value >= 70) return 'bg-amber-500'
|
||||
return 'bg-foreground'
|
||||
})
|
||||
|
||||
const cacheHitRate = computed(() => {
|
||||
const rate = info.value?.cache_stats?.cache_hit_rate ?? 0
|
||||
return rate.toFixed(1)
|
||||
})
|
||||
|
||||
const skills = computed(() => info.value?.skills ?? [])
|
||||
|
||||
function formatTokenCount(n: number): string {
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
||||
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
|
||||
return String(n)
|
||||
}
|
||||
|
||||
function openSkillFile(skillName: string) {
|
||||
openInFileManager?.(`/data/skills/${skillName}/SKILL.md`, false)
|
||||
}
|
||||
</script>
|
||||
@@ -256,6 +256,7 @@ func runServe() {
|
||||
provideServerHandler(handlers.NewMCPOAuthHandler),
|
||||
provideOAuthService,
|
||||
provideServerHandler(handlers.NewTokenUsageHandler),
|
||||
provideServerHandler(handlers.NewSessionInfoHandler),
|
||||
provideServerHandler(handlers.NewBrowserContextsHandler),
|
||||
provideServerHandler(handlers.NewSupermarketHandler),
|
||||
provideServerHandler(provideCLIHandler),
|
||||
|
||||
@@ -183,6 +183,7 @@ func runServe() {
|
||||
provideServerHandler(handlers.NewMCPOAuthHandler),
|
||||
provideOAuthService,
|
||||
provideServerHandler(handlers.NewTokenUsageHandler),
|
||||
provideServerHandler(handlers.NewSessionInfoHandler),
|
||||
provideServerHandler(handlers.NewBrowserContextsHandler),
|
||||
provideServerHandler(provideCLIHandler),
|
||||
provideServerHandler(provideWebHandler),
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
-- name: CountMessagesBySession :one
|
||||
SELECT COUNT(*)::bigint AS message_count
|
||||
FROM bot_history_messages
|
||||
WHERE session_id = sqlc.arg(session_id);
|
||||
|
||||
-- name: GetLatestAssistantUsage :one
|
||||
SELECT
|
||||
COALESCE((m.usage->>'inputTokens')::bigint, 0)::bigint AS input_tokens
|
||||
FROM bot_history_messages m
|
||||
WHERE m.session_id = sqlc.arg(session_id)
|
||||
AND m.role = 'assistant'
|
||||
AND m.usage IS NOT NULL
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: GetSessionCacheStats :one
|
||||
SELECT
|
||||
COALESCE(SUM((m.usage->>'inputTokens')::bigint), 0)::bigint AS total_input_tokens,
|
||||
COALESCE(SUM((m.usage->'inputTokenDetails'->>'cacheReadTokens')::bigint), 0)::bigint AS cache_read_tokens,
|
||||
COALESCE(SUM((m.usage->'inputTokenDetails'->>'cacheWriteTokens')::bigint), 0)::bigint AS cache_write_tokens
|
||||
FROM bot_history_messages m
|
||||
WHERE m.session_id = sqlc.arg(session_id)
|
||||
AND m.usage IS NOT NULL;
|
||||
|
||||
-- name: GetSessionUsedSkills :many
|
||||
SELECT DISTINCT
|
||||
(part->'input'->>'skillName')::text AS skill_name
|
||||
FROM bot_history_messages m,
|
||||
jsonb_array_elements(
|
||||
CASE WHEN jsonb_typeof(m.content->'content') = 'array'
|
||||
THEN m.content->'content'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS part
|
||||
WHERE m.session_id = sqlc.arg(session_id)
|
||||
AND m.role = 'assistant'
|
||||
AND part->>'type' = 'tool-call'
|
||||
AND part->>'toolName' = 'use_skill'
|
||||
AND part->'input'->>'skillName' IS NOT NULL
|
||||
AND part->'input'->>'skillName' != ''
|
||||
ORDER BY skill_name;
|
||||
@@ -0,0 +1,105 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: session_info.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const countMessagesBySession = `-- name: CountMessagesBySession :one
|
||||
SELECT COUNT(*)::bigint AS message_count
|
||||
FROM bot_history_messages
|
||||
WHERE session_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) CountMessagesBySession(ctx context.Context, sessionID pgtype.UUID) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countMessagesBySession, sessionID)
|
||||
var message_count int64
|
||||
err := row.Scan(&message_count)
|
||||
return message_count, err
|
||||
}
|
||||
|
||||
const getLatestAssistantUsage = `-- name: GetLatestAssistantUsage :one
|
||||
SELECT
|
||||
COALESCE((m.usage->>'inputTokens')::bigint, 0)::bigint AS input_tokens
|
||||
FROM bot_history_messages m
|
||||
WHERE m.session_id = $1
|
||||
AND m.role = 'assistant'
|
||||
AND m.usage IS NOT NULL
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLatestAssistantUsage(ctx context.Context, sessionID pgtype.UUID) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, getLatestAssistantUsage, sessionID)
|
||||
var input_tokens int64
|
||||
err := row.Scan(&input_tokens)
|
||||
return input_tokens, err
|
||||
}
|
||||
|
||||
const getSessionCacheStats = `-- name: GetSessionCacheStats :one
|
||||
SELECT
|
||||
COALESCE(SUM((m.usage->>'inputTokens')::bigint), 0)::bigint AS total_input_tokens,
|
||||
COALESCE(SUM((m.usage->'inputTokenDetails'->>'cacheReadTokens')::bigint), 0)::bigint AS cache_read_tokens,
|
||||
COALESCE(SUM((m.usage->'inputTokenDetails'->>'cacheWriteTokens')::bigint), 0)::bigint AS cache_write_tokens
|
||||
FROM bot_history_messages m
|
||||
WHERE m.session_id = $1
|
||||
AND m.usage IS NOT NULL
|
||||
`
|
||||
|
||||
type GetSessionCacheStatsRow struct {
|
||||
TotalInputTokens int64 `json:"total_input_tokens"`
|
||||
CacheReadTokens int64 `json:"cache_read_tokens"`
|
||||
CacheWriteTokens int64 `json:"cache_write_tokens"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSessionCacheStats(ctx context.Context, sessionID pgtype.UUID) (GetSessionCacheStatsRow, error) {
|
||||
row := q.db.QueryRow(ctx, getSessionCacheStats, sessionID)
|
||||
var i GetSessionCacheStatsRow
|
||||
err := row.Scan(&i.TotalInputTokens, &i.CacheReadTokens, &i.CacheWriteTokens)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getSessionUsedSkills = `-- name: GetSessionUsedSkills :many
|
||||
SELECT DISTINCT
|
||||
(part->'input'->>'skillName')::text AS skill_name
|
||||
FROM bot_history_messages m,
|
||||
jsonb_array_elements(
|
||||
CASE WHEN jsonb_typeof(m.content->'content') = 'array'
|
||||
THEN m.content->'content'
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
) AS part
|
||||
WHERE m.session_id = $1
|
||||
AND m.role = 'assistant'
|
||||
AND part->>'type' = 'tool-call'
|
||||
AND part->>'toolName' = 'use_skill'
|
||||
AND part->'input'->>'skillName' IS NOT NULL
|
||||
AND part->'input'->>'skillName' != ''
|
||||
ORDER BY skill_name
|
||||
`
|
||||
|
||||
func (q *Queries) GetSessionUsedSkills(ctx context.Context, sessionID pgtype.UUID) ([]string, error) {
|
||||
rows, err := q.db.Query(ctx, getSessionUsedSkills, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []string
|
||||
for rows.Next() {
|
||||
var skill_name string
|
||||
if err := rows.Scan(&skill_name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, skill_name)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
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/info", 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}/info [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
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -719,6 +719,13 @@ export type HandlersBrowserCoresResponse = {
|
||||
cores?: Array<string>;
|
||||
};
|
||||
|
||||
export type HandlersCacheStats = {
|
||||
cache_hit_rate?: number;
|
||||
cache_read_tokens?: number;
|
||||
cache_write_tokens?: number;
|
||||
total_input_tokens?: number;
|
||||
};
|
||||
|
||||
export type HandlersChannelMeta = {
|
||||
capabilities?: ChannelChannelCapabilities;
|
||||
config_schema?: ChannelConfigSchema;
|
||||
@@ -729,6 +736,11 @@ export type HandlersChannelMeta = {
|
||||
user_config_schema?: ChannelConfigSchema;
|
||||
};
|
||||
|
||||
export type HandlersContextUsage = {
|
||||
context_window?: number;
|
||||
used_tokens?: number;
|
||||
};
|
||||
|
||||
export type HandlersCreateContainerRequest = {
|
||||
image?: string;
|
||||
restore_data?: boolean;
|
||||
@@ -916,6 +928,13 @@ export type HandlersRollbackRequest = {
|
||||
version?: number;
|
||||
};
|
||||
|
||||
export type HandlersSessionInfoResponse = {
|
||||
cache_stats?: HandlersCacheStats;
|
||||
context_usage?: HandlersContextUsage;
|
||||
message_count?: number;
|
||||
skills?: Array<string>;
|
||||
};
|
||||
|
||||
export type HandlersSkillItem = {
|
||||
content?: string;
|
||||
description?: string;
|
||||
@@ -5237,6 +5256,53 @@ export type PatchBotsByBotIdSessionsBySessionIdResponses = {
|
||||
|
||||
export type PatchBotsByBotIdSessionsBySessionIdResponse = PatchBotsByBotIdSessionsBySessionIdResponses[keyof PatchBotsByBotIdSessionsBySessionIdResponses];
|
||||
|
||||
export type GetBotsByBotIdSessionsBySessionIdInfoData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Bot ID
|
||||
*/
|
||||
bot_id: string;
|
||||
/**
|
||||
* Session ID
|
||||
*/
|
||||
session_id: string;
|
||||
};
|
||||
query?: {
|
||||
/**
|
||||
* Optional model UUID override for context window
|
||||
*/
|
||||
model_id?: string;
|
||||
};
|
||||
url: '/bots/{bot_id}/sessions/{session_id}/info';
|
||||
};
|
||||
|
||||
export type GetBotsByBotIdSessionsBySessionIdInfoErrors = {
|
||||
/**
|
||||
* Bad Request
|
||||
*/
|
||||
400: HandlersErrorResponse;
|
||||
/**
|
||||
* Forbidden
|
||||
*/
|
||||
403: HandlersErrorResponse;
|
||||
/**
|
||||
* Internal Server Error
|
||||
*/
|
||||
500: HandlersErrorResponse;
|
||||
};
|
||||
|
||||
export type GetBotsByBotIdSessionsBySessionIdInfoError = GetBotsByBotIdSessionsBySessionIdInfoErrors[keyof GetBotsByBotIdSessionsBySessionIdInfoErrors];
|
||||
|
||||
export type GetBotsByBotIdSessionsBySessionIdInfoResponses = {
|
||||
/**
|
||||
* OK
|
||||
*/
|
||||
200: HandlersSessionInfoResponse;
|
||||
};
|
||||
|
||||
export type GetBotsByBotIdSessionsBySessionIdInfoResponse = GetBotsByBotIdSessionsBySessionIdInfoResponses[keyof GetBotsByBotIdSessionsBySessionIdInfoResponses];
|
||||
|
||||
export type DeleteBotsByBotIdSettingsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
||||
+105
@@ -4395,6 +4395,63 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/sessions/{session_id}/info": {
|
||||
"get": {
|
||||
"description": "Get aggregated info for a chat session including message count, context usage, cache stats, and used skills",
|
||||
"tags": [
|
||||
"sessions"
|
||||
],
|
||||
"summary": "Get session info",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Session ID",
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Optional model UUID override for context window",
|
||||
"name": "model_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.SessionInfoResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/settings": {
|
||||
"get": {
|
||||
"description": "Get agent settings for current user",
|
||||
@@ -10875,6 +10932,23 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.CacheStats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cache_hit_rate": {
|
||||
"type": "number"
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cache_write_tokens": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_input_tokens": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.ChannelMeta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -10901,6 +10975,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.ContextUsage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context_window": {
|
||||
"type": "integer"
|
||||
},
|
||||
"used_tokens": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.CreateContainerRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11359,6 +11444,26 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.SessionInfoResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cache_stats": {
|
||||
"$ref": "#/definitions/handlers.CacheStats"
|
||||
},
|
||||
"context_usage": {
|
||||
"$ref": "#/definitions/handlers.ContextUsage"
|
||||
},
|
||||
"message_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.SkillItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -4386,6 +4386,63 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/sessions/{session_id}/info": {
|
||||
"get": {
|
||||
"description": "Get aggregated info for a chat session including message count, context usage, cache stats, and used skills",
|
||||
"tags": [
|
||||
"sessions"
|
||||
],
|
||||
"summary": "Get session info",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Bot ID",
|
||||
"name": "bot_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Session ID",
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Optional model UUID override for context window",
|
||||
"name": "model_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.SessionInfoResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handlers.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/settings": {
|
||||
"get": {
|
||||
"description": "Get agent settings for current user",
|
||||
@@ -10866,6 +10923,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.CacheStats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cache_hit_rate": {
|
||||
"type": "number"
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"type": "integer"
|
||||
},
|
||||
"cache_write_tokens": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_input_tokens": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.ChannelMeta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -10892,6 +10966,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.ContextUsage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context_window": {
|
||||
"type": "integer"
|
||||
},
|
||||
"used_tokens": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.CreateContainerRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11350,6 +11435,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.SessionInfoResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cache_stats": {
|
||||
"$ref": "#/definitions/handlers.CacheStats"
|
||||
},
|
||||
"context_usage": {
|
||||
"$ref": "#/definitions/handlers.ContextUsage"
|
||||
},
|
||||
"message_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.SkillItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1190,6 +1190,17 @@ definitions:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
handlers.CacheStats:
|
||||
properties:
|
||||
cache_hit_rate:
|
||||
type: number
|
||||
cache_read_tokens:
|
||||
type: integer
|
||||
cache_write_tokens:
|
||||
type: integer
|
||||
total_input_tokens:
|
||||
type: integer
|
||||
type: object
|
||||
handlers.ChannelMeta:
|
||||
properties:
|
||||
capabilities:
|
||||
@@ -1207,6 +1218,13 @@ definitions:
|
||||
user_config_schema:
|
||||
$ref: '#/definitions/channel.ConfigSchema'
|
||||
type: object
|
||||
handlers.ContextUsage:
|
||||
properties:
|
||||
context_window:
|
||||
type: integer
|
||||
used_tokens:
|
||||
type: integer
|
||||
type: object
|
||||
handlers.CreateContainerRequest:
|
||||
properties:
|
||||
image:
|
||||
@@ -1503,6 +1521,19 @@ definitions:
|
||||
version:
|
||||
type: integer
|
||||
type: object
|
||||
handlers.SessionInfoResponse:
|
||||
properties:
|
||||
cache_stats:
|
||||
$ref: '#/definitions/handlers.CacheStats'
|
||||
context_usage:
|
||||
$ref: '#/definitions/handlers.ContextUsage'
|
||||
message_count:
|
||||
type: integer
|
||||
skills:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
handlers.SkillItem:
|
||||
properties:
|
||||
content:
|
||||
@@ -5655,6 +5686,45 @@ paths:
|
||||
summary: Update a session
|
||||
tags:
|
||||
- sessions
|
||||
/bots/{bot_id}/sessions/{session_id}/info:
|
||||
get:
|
||||
description: Get aggregated info for a chat session including message count,
|
||||
context usage, cache stats, and used skills
|
||||
parameters:
|
||||
- description: Bot ID
|
||||
in: path
|
||||
name: bot_id
|
||||
required: true
|
||||
type: string
|
||||
- description: Session ID
|
||||
in: path
|
||||
name: session_id
|
||||
required: true
|
||||
type: string
|
||||
- description: Optional model UUID override for context window
|
||||
in: query
|
||||
name: model_id
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.SessionInfoResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"403":
|
||||
description: Forbidden
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ErrorResponse'
|
||||
summary: Get session info
|
||||
tags:
|
||||
- sessions
|
||||
/bots/{bot_id}/settings:
|
||||
delete:
|
||||
description: Remove agent settings for current user
|
||||
|
||||
Reference in New Issue
Block a user