mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
80b36f79f3
- Rewrite SQL queries to join bot_history_messages with bot_sessions, supporting chat/heartbeat/schedule usage from a single source - Update Go handler and CLI command to use unified queries - Fix daily chart stacking: each session type gets its own bar group - Add total input/output trend lines to the daily token chart - Fix summary cards reactivity by restricting aggregation to allDays range - Fix cache chart reactive dependency tracking by inlining data access - Add i18n keys for schedule, totalInput, totalOutput - Default time range changed to 7 days - Regenerate sqlc, swagger, and SDK
132 lines
3.3 KiB
Go
132 lines
3.3 KiB
Go
package command
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
|
|
dbsqlc "github.com/memohai/memoh/internal/db/sqlc"
|
|
)
|
|
|
|
func (h *Handler) buildUsageGroup() *CommandGroup {
|
|
g := newCommandGroup("usage", "View token usage")
|
|
g.DefaultAction = "summary"
|
|
g.Register(SubCommand{
|
|
Name: "summary",
|
|
Usage: "summary - Token usage summary (last 7 days)",
|
|
Handler: func(cc CommandContext) (string, error) {
|
|
botUUID, err := parseBotUUID(cc.BotID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
now := time.Now().UTC()
|
|
from := now.AddDate(0, 0, -7)
|
|
fromTS := pgtype.Timestamptz{Time: from, Valid: true}
|
|
toTS := pgtype.Timestamptz{Time: now, Valid: true}
|
|
nullModel := pgtype.UUID{Valid: false}
|
|
|
|
rows, err := h.queries.GetTokenUsageByDayAndType(cc.Ctx, dbsqlc.GetTokenUsageByDayAndTypeParams{
|
|
BotID: botUUID, FromTime: fromTS, ToTime: toTS, ModelID: nullModel,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(rows) == 0 {
|
|
return "No token usage in the last 7 days.", nil
|
|
}
|
|
|
|
type bucket struct {
|
|
label string
|
|
rows []dbsqlc.GetTokenUsageByDayAndTypeRow
|
|
}
|
|
buckets := []bucket{
|
|
{label: "Chat"},
|
|
{label: "Heartbeat"},
|
|
{label: "Schedule"},
|
|
}
|
|
for _, r := range rows {
|
|
switch r.SessionType {
|
|
case "heartbeat":
|
|
buckets[1].rows = append(buckets[1].rows, r)
|
|
case "schedule":
|
|
buckets[2].rows = append(buckets[2].rows, r)
|
|
default:
|
|
buckets[0].rows = append(buckets[0].rows, r)
|
|
}
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString("Token usage (last 7 days):\n\n")
|
|
|
|
first := true
|
|
for _, bk := range buckets {
|
|
if len(bk.rows) == 0 {
|
|
continue
|
|
}
|
|
if !first {
|
|
b.WriteByte('\n')
|
|
}
|
|
first = false
|
|
b.WriteString(bk.label + ":\n")
|
|
var totalIn, totalOut int64
|
|
for _, r := range bk.rows {
|
|
day := r.Day.Time.Format("01-02")
|
|
fmt.Fprintf(&b, " %s: in=%d out=%d\n", day, r.InputTokens, r.OutputTokens)
|
|
totalIn += r.InputTokens
|
|
totalOut += r.OutputTokens
|
|
}
|
|
fmt.Fprintf(&b, " Total: in=%d out=%d\n", totalIn, totalOut)
|
|
}
|
|
|
|
return strings.TrimRight(b.String(), "\n"), nil
|
|
},
|
|
})
|
|
g.Register(SubCommand{
|
|
Name: "by-model",
|
|
Usage: "by-model - Token usage grouped by model",
|
|
Handler: func(cc CommandContext) (string, error) {
|
|
botUUID, err := parseBotUUID(cc.BotID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
now := time.Now().UTC()
|
|
from := now.AddDate(0, 0, -7)
|
|
fromTS := pgtype.Timestamptz{Time: from, Valid: true}
|
|
toTS := pgtype.Timestamptz{Time: now, Valid: true}
|
|
|
|
rows, err := h.queries.GetTokenUsageByModel(cc.Ctx, dbsqlc.GetTokenUsageByModelParams{
|
|
BotID: botUUID, FromTime: fromTS, ToTime: toTS,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(rows) == 0 {
|
|
return "No token usage in the last 7 days.", nil
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString("Token usage by model (last 7 days):\n\n")
|
|
|
|
for _, r := range rows {
|
|
fmt.Fprintf(&b, " %s (%s): in=%d out=%d\n", r.ModelName, r.ProviderName, r.InputTokens, r.OutputTokens)
|
|
}
|
|
|
|
return strings.TrimRight(b.String(), "\n"), nil
|
|
},
|
|
})
|
|
return g
|
|
}
|
|
|
|
func parseBotUUID(botID string) (pgtype.UUID, error) {
|
|
parsed, err := uuid.Parse(botID)
|
|
if err != nil {
|
|
return pgtype.UUID{}, fmt.Errorf("invalid bot ID: %w", err)
|
|
}
|
|
return pgtype.UUID{Bytes: parsed, Valid: true}, nil
|
|
}
|