mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
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.
This commit is contained in:
@@ -289,11 +289,11 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="activeRightTab === 'info'"
|
||||
v-if="activeRightTab === 'status'"
|
||||
class="absolute inset-0"
|
||||
>
|
||||
<SessionInfoPanel
|
||||
:visible="activeRightTab === 'info'"
|
||||
:visible="activeRightTab === 'status'"
|
||||
:override-model-id="overrideModelId"
|
||||
/>
|
||||
</div>
|
||||
@@ -398,7 +398,7 @@ const reasoningPopoverOpen = ref(false)
|
||||
|
||||
// ---- Right sidebar panel ----
|
||||
|
||||
type RightTabId = 'terminal' | 'files' | 'info'
|
||||
type RightTabId = 'terminal' | 'files' | 'status'
|
||||
|
||||
interface RightTab {
|
||||
id: RightTabId
|
||||
@@ -409,7 +409,7 @@ interface RightTab {
|
||||
const rightTabs = computed<RightTab[]>(() => [
|
||||
{ id: 'terminal', label: 'Terminal', icon: TerminalSquare },
|
||||
{ id: 'files', label: t('chat.files'), icon: FolderOpen },
|
||||
{ id: 'info', label: 'Info', icon: BarChart3 },
|
||||
{ id: 'status', label: 'Status', icon: BarChart3 },
|
||||
])
|
||||
|
||||
const activeRightTab = ref<RightTabId | null>(null)
|
||||
|
||||
@@ -104,7 +104,7 @@ 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 { getBotsByBotIdSessionsBySessionIdStatus } from '@memohai/sdk'
|
||||
import type { HandlersSessionInfoResponse } from '@memohai/sdk'
|
||||
import { useChatStore } from '@/store/chat-list'
|
||||
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
|
||||
@@ -119,9 +119,9 @@ const { currentBotId, sessionId } = storeToRefs(chatStore)
|
||||
const openInFileManager = inject(openInFileManagerKey, undefined)
|
||||
|
||||
const { data: info } = useQuery({
|
||||
key: () => ['session-info', currentBotId.value ?? '', sessionId.value ?? '', props.overrideModelId ?? ''],
|
||||
key: () => ['session-status', currentBotId.value ?? '', sessionId.value ?? '', props.overrideModelId ?? ''],
|
||||
query: async () => {
|
||||
const { data } = await getBotsByBotIdSessionsBySessionIdInfo({
|
||||
const { data } = await getBotsByBotIdSessionsBySessionIdStatus({
|
||||
path: {
|
||||
bot_id: currentBotId.value!,
|
||||
session_id: sessionId.value!,
|
||||
|
||||
@@ -22,6 +22,15 @@ FROM bot_history_messages m
|
||||
WHERE m.session_id = sqlc.arg(session_id)
|
||||
AND m.usage IS NOT NULL;
|
||||
|
||||
-- name: GetLatestSessionIDByBot :one
|
||||
SELECT s.id
|
||||
FROM bot_sessions s
|
||||
WHERE s.bot_id = sqlc.arg(bot_id)
|
||||
AND s.type = 'chat'
|
||||
AND s.deleted_at IS NULL
|
||||
ORDER BY s.updated_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: GetSessionUsedSkills :many
|
||||
SELECT DISTINCT
|
||||
(part->'input'->>'skillName')::text AS skill_name
|
||||
|
||||
@@ -15,5 +15,6 @@ func (h *Handler) buildRegistry() *Registry {
|
||||
r.RegisterGroup(h.buildHeartbeatGroup())
|
||||
r.RegisterGroup(h.buildSkillGroup())
|
||||
r.RegisterGroup(h.buildFSGroup())
|
||||
r.RegisterGroup(h.buildStatusGroup())
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func (h *Handler) buildStatusGroup() *CommandGroup {
|
||||
g := newCommandGroup("status", "View current session status")
|
||||
g.DefaultAction = "show"
|
||||
g.Register(SubCommand{
|
||||
Name: "show",
|
||||
Usage: "show - Show current session status",
|
||||
Handler: func(cc CommandContext) (string, error) {
|
||||
botUUID, err := parseBotUUID(cc.BotID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sessionID, err := h.queries.GetLatestSessionIDByBot(cc.Ctx, botUUID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "No active session found.", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
msgCount, err := h.queries.CountMessagesBySession(cc.Ctx, sessionID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("count messages: %w", err)
|
||||
}
|
||||
|
||||
var usedTokens int64
|
||||
latestUsage, err := h.queries.GetLatestAssistantUsage(cc.Ctx, sessionID)
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", fmt.Errorf("get usage: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
usedTokens = latestUsage
|
||||
}
|
||||
|
||||
cacheRow, err := h.queries.GetSessionCacheStats(cc.Ctx, sessionID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get cache: %w", err)
|
||||
}
|
||||
|
||||
var contextWindowStr string
|
||||
if h.settingsService != nil {
|
||||
s, sErr := h.settingsService.GetBot(cc.Ctx, cc.BotID)
|
||||
if sErr == nil && s.ChatModelID != "" && h.modelsService != nil {
|
||||
m, mErr := h.modelsService.GetByID(cc.Ctx, s.ChatModelID)
|
||||
if mErr == nil && m.Config.ContextWindow != nil {
|
||||
contextWindowStr = formatTokens(int64(*m.Config.ContextWindow))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cacheHitRate float64
|
||||
if cacheRow.TotalInputTokens > 0 {
|
||||
cacheHitRate = float64(cacheRow.CacheReadTokens) / float64(cacheRow.TotalInputTokens) * 100
|
||||
}
|
||||
|
||||
skills, _ := h.queries.GetSessionUsedSkills(cc.Ctx, sessionID)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("Session Status:\n\n")
|
||||
fmt.Fprintf(&b, "- Messages: %d\n", msgCount)
|
||||
if contextWindowStr != "" {
|
||||
fmt.Fprintf(&b, "- Context: %s / %s\n", formatTokens(usedTokens), contextWindowStr)
|
||||
} else {
|
||||
fmt.Fprintf(&b, "- Context: %s\n", formatTokens(usedTokens))
|
||||
}
|
||||
fmt.Fprintf(&b, "- Cache Hit Rate: %.1f%%\n", cacheHitRate)
|
||||
fmt.Fprintf(&b, "- Cache Read: %s\n", formatTokens(cacheRow.CacheReadTokens))
|
||||
fmt.Fprintf(&b, "- Cache Write: %s\n", formatTokens(cacheRow.CacheWriteTokens))
|
||||
|
||||
if len(skills) > 0 {
|
||||
fmt.Fprintf(&b, "- Skills: %s\n", strings.Join(skills, ", "))
|
||||
}
|
||||
|
||||
return strings.TrimRight(b.String(), "\n"), nil
|
||||
},
|
||||
})
|
||||
return g
|
||||
}
|
||||
|
||||
func formatTokens(n int64) string {
|
||||
if n >= 1_000_000 {
|
||||
return fmt.Sprintf("%.1fM", float64(n)/1_000_000)
|
||||
}
|
||||
if n >= 1_000 {
|
||||
return fmt.Sprintf("%.1fK", float64(n)/1_000)
|
||||
}
|
||||
return strconv.FormatInt(n, 10)
|
||||
}
|
||||
@@ -42,6 +42,23 @@ func (q *Queries) GetLatestAssistantUsage(ctx context.Context, sessionID pgtype.
|
||||
return input_tokens, err
|
||||
}
|
||||
|
||||
const getLatestSessionIDByBot = `-- name: GetLatestSessionIDByBot :one
|
||||
SELECT s.id
|
||||
FROM bot_sessions s
|
||||
WHERE s.bot_id = $1
|
||||
AND s.type = 'chat'
|
||||
AND s.deleted_at IS NULL
|
||||
ORDER BY s.updated_at DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLatestSessionIDByBot(ctx context.Context, botID pgtype.UUID) (pgtype.UUID, error) {
|
||||
row := q.db.QueryRow(ctx, getLatestSessionIDByBot, botID)
|
||||
var id pgtype.UUID
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const getSessionCacheStats = `-- name: GetSessionCacheStats :one
|
||||
SELECT
|
||||
COALESCE(SUM((m.usage->>'inputTokens')::bigint), 0)::bigint AS total_input_tokens,
|
||||
|
||||
@@ -38,7 +38,7 @@ func NewSessionInfoHandler(log *slog.Logger, queries *sqlc.Queries, botService *
|
||||
}
|
||||
|
||||
func (h *SessionInfoHandler) Register(e *echo.Echo) {
|
||||
e.GET("/bots/:bot_id/sessions/:session_id/info", h.GetSessionInfo)
|
||||
e.GET("/bots/:bot_id/sessions/:session_id/status", h.GetSessionInfo)
|
||||
}
|
||||
|
||||
type SessionInfoResponse struct {
|
||||
@@ -71,7 +71,7 @@ type CacheStats struct {
|
||||
// @Failure 400 {object} ErrorResponse
|
||||
// @Failure 403 {object} ErrorResponse
|
||||
// @Failure 500 {object} ErrorResponse
|
||||
// @Router /bots/{bot_id}/sessions/{session_id}/info [get].
|
||||
// @Router /bots/{bot_id}/sessions/{session_id}/status [get].
|
||||
func (h *SessionInfoHandler) GetSessionInfo(c echo.Context) error {
|
||||
userID, err := RequireChannelIdentityID(c)
|
||||
if err != nil {
|
||||
|
||||
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
@@ -5256,7 +5256,7 @@ export type PatchBotsByBotIdSessionsBySessionIdResponses = {
|
||||
|
||||
export type PatchBotsByBotIdSessionsBySessionIdResponse = PatchBotsByBotIdSessionsBySessionIdResponses[keyof PatchBotsByBotIdSessionsBySessionIdResponses];
|
||||
|
||||
export type GetBotsByBotIdSessionsBySessionIdInfoData = {
|
||||
export type GetBotsByBotIdSessionsBySessionIdStatusData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
@@ -5274,10 +5274,10 @@ export type GetBotsByBotIdSessionsBySessionIdInfoData = {
|
||||
*/
|
||||
model_id?: string;
|
||||
};
|
||||
url: '/bots/{bot_id}/sessions/{session_id}/info';
|
||||
url: '/bots/{bot_id}/sessions/{session_id}/status';
|
||||
};
|
||||
|
||||
export type GetBotsByBotIdSessionsBySessionIdInfoErrors = {
|
||||
export type GetBotsByBotIdSessionsBySessionIdStatusErrors = {
|
||||
/**
|
||||
* Bad Request
|
||||
*/
|
||||
@@ -5292,16 +5292,16 @@ export type GetBotsByBotIdSessionsBySessionIdInfoErrors = {
|
||||
500: HandlersErrorResponse;
|
||||
};
|
||||
|
||||
export type GetBotsByBotIdSessionsBySessionIdInfoError = GetBotsByBotIdSessionsBySessionIdInfoErrors[keyof GetBotsByBotIdSessionsBySessionIdInfoErrors];
|
||||
export type GetBotsByBotIdSessionsBySessionIdStatusError = GetBotsByBotIdSessionsBySessionIdStatusErrors[keyof GetBotsByBotIdSessionsBySessionIdStatusErrors];
|
||||
|
||||
export type GetBotsByBotIdSessionsBySessionIdInfoResponses = {
|
||||
export type GetBotsByBotIdSessionsBySessionIdStatusResponses = {
|
||||
/**
|
||||
* OK
|
||||
*/
|
||||
200: HandlersSessionInfoResponse;
|
||||
};
|
||||
|
||||
export type GetBotsByBotIdSessionsBySessionIdInfoResponse = GetBotsByBotIdSessionsBySessionIdInfoResponses[keyof GetBotsByBotIdSessionsBySessionIdInfoResponses];
|
||||
export type GetBotsByBotIdSessionsBySessionIdStatusResponse = GetBotsByBotIdSessionsBySessionIdStatusResponses[keyof GetBotsByBotIdSessionsBySessionIdStatusResponses];
|
||||
|
||||
export type DeleteBotsByBotIdSettingsData = {
|
||||
body?: never;
|
||||
|
||||
+1
-1
@@ -4395,7 +4395,7 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/sessions/{session_id}/info": {
|
||||
"/bots/{bot_id}/sessions/{session_id}/status": {
|
||||
"get": {
|
||||
"description": "Get aggregated info for a chat session including message count, context usage, cache stats, and used skills",
|
||||
"tags": [
|
||||
|
||||
+1
-1
@@ -4386,7 +4386,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/bots/{bot_id}/sessions/{session_id}/info": {
|
||||
"/bots/{bot_id}/sessions/{session_id}/status": {
|
||||
"get": {
|
||||
"description": "Get aggregated info for a chat session including message count, context usage, cache stats, and used skills",
|
||||
"tags": [
|
||||
|
||||
+1
-1
@@ -5686,7 +5686,7 @@ paths:
|
||||
summary: Update a session
|
||||
tags:
|
||||
- sessions
|
||||
/bots/{bot_id}/sessions/{session_id}/info:
|
||||
/bots/{bot_id}/sessions/{session_id}/status:
|
||||
get:
|
||||
description: Get aggregated info for a chat session including message count,
|
||||
context usage, cache stats, and used skills
|
||||
|
||||
Reference in New Issue
Block a user