feat(admin): add LLM call logs, system info, plugin list pages
- ai-core/llm: ring-buffer call logger with stats - Gateway: /api/v1/admin/llm-calls, /system, /plugins, /sessions endpoints - Frontend: LLMCallsPage (table + stats), SystemPage (version/plugins/sessions) - Admin nav expanded to 7 items
This commit is contained in:
1 parent
8bfda096f7
commit
1a80ac2a00
7 files changed
+318
-1
No files matched your search
@@ -32,6 +32,7 @@ import (
|
||||
websearch "git.yeij.top/AskaEth/Cyrene_v2/plugins/web-search"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/llm"
|
||||
llmlog "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/llm"
|
||||
aiCoreCtx "git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/context"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/orchestrator"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/persona"
|
||||
@@ -231,9 +232,12 @@ func main() {
|
||||
authMgr := api.NewRealAuthManager(cfg.JWTSecret, cfg.JWTExpiryHours, userStore)
|
||||
|
||||
// Build dependencies for gateway handlers
|
||||
llmLogger := llmlog.NewLogger(200)
|
||||
gwDeps := api.Dependencies{
|
||||
AuthMgr: authMgr,
|
||||
Orchestrator: orch,
|
||||
LLMLogger: &llmLoggerAdapter{Logger: llmLogger},
|
||||
PluginList: pluginNamesToMap(reg.ListNames()),
|
||||
}
|
||||
if memSvc != nil {
|
||||
gwDeps.MemorySvc = memory.NewAPIAdapter(memSvc)
|
||||
@@ -276,4 +280,16 @@ func main() {
|
||||
_ = voiceSvc
|
||||
_ = memSvc
|
||||
_ = orch
|
||||
}
|
||||
}
|
||||
// llmLoggerAdapter adapts llm.Logger to api.LLMCallLogger interface.
|
||||
type llmLoggerAdapter struct{ *llmlog.Logger }
|
||||
|
||||
func (a *llmLoggerAdapter) Recent(n int) []map[string]any { return a.Logger.RecentMaps(n) }
|
||||
|
||||
func pluginNamesToMap(names []string) []map[string]string {
|
||||
result := make([]map[string]string, len(names))
|
||||
for i, n := range names {
|
||||
result[i] = map[string]string{"name": n, "enabled": "true"}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CallRecord struct {
|
||||
ID string `json:"id"`
|
||||
Time time.Time `json:"time"`
|
||||
Model string `json:"model"`
|
||||
Purpose string `json:"purpose"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompTokens int `json:"completion_tokens"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
mu sync.RWMutex
|
||||
records []CallRecord
|
||||
max int
|
||||
index int
|
||||
total int64
|
||||
}
|
||||
|
||||
func NewLogger(max int) *Logger { return &Logger{records: make([]CallRecord, max), max: max} }
|
||||
|
||||
func (l *Logger) Record(r CallRecord) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
r.ID = time.Now().Format("150405") + "-" + r.Purpose
|
||||
l.records[l.index%l.max] = r
|
||||
l.index++
|
||||
l.total++
|
||||
}
|
||||
|
||||
func (l *Logger) Recent(n int) []CallRecord {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
if n > l.max { n = l.max }
|
||||
count := l.index
|
||||
if count > l.max { count = l.max }
|
||||
if n > count { n = count }
|
||||
result := make([]CallRecord, n)
|
||||
for i := 0; i < n; i++ {
|
||||
idx := (l.index - 1 - i + l.max) % l.max
|
||||
if idx < l.max { result[n-1-i] = l.records[idx] }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (l *Logger) RecentMaps(n int) []map[string]any {
|
||||
records := l.Recent(n)
|
||||
result := make([]map[string]any, len(records))
|
||||
for i, r := range records {
|
||||
result[i] = map[string]any{
|
||||
"id": r.ID, "time": r.Time.Format(time.RFC3339), "model": r.Model,
|
||||
"purpose": r.Purpose, "prompt_tokens": r.PromptTokens, "completion_tokens": r.CompTokens,
|
||||
"duration_ms": r.DurationMs, "success": r.Success, "error": r.Error,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (l *Logger) Stats() map[string]any {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
var calls, succ int64
|
||||
var dur, tok int64
|
||||
count := l.index
|
||||
if count > l.max { count = l.max }
|
||||
for i := 0; i < count; i++ {
|
||||
r := l.records[(l.index-1-i+l.max)%l.max]
|
||||
calls++
|
||||
if r.Success { succ++ }
|
||||
dur += r.DurationMs
|
||||
tok += int64(r.PromptTokens + r.CompTokens)
|
||||
}
|
||||
avg := 0
|
||||
if calls > 0 { avg = int(dur / calls) }
|
||||
return map[string]any{"total_calls": calls, "success_calls": succ, "avg_duration_ms": avg, "total_tokens": tok}
|
||||
}
|
||||
@@ -56,6 +56,13 @@ type OrchestratorPipeline interface {
|
||||
// WebSocketHub manages WS connections.
|
||||
type WebSocketHub interface {
|
||||
UpgradeHandler() http.HandlerFunc
|
||||
Count() int
|
||||
}
|
||||
|
||||
// LLMCallLogger provides LLM call history.
|
||||
type LLMCallLogger interface {
|
||||
Recent(n int) []map[string]any
|
||||
Stats() map[string]any
|
||||
}
|
||||
|
||||
// Dependencies holds all injectable services.
|
||||
@@ -65,6 +72,8 @@ type Dependencies struct {
|
||||
VoiceSvc VoiceService
|
||||
Orchestrator OrchestratorPipeline
|
||||
WSHub WebSocketHub
|
||||
LLMLogger LLMCallLogger
|
||||
PluginList []map[string]string
|
||||
}
|
||||
|
||||
// Router handles HTTP routing with middleware support.
|
||||
@@ -101,6 +110,10 @@ func (r *Router) setupRoutes() {
|
||||
|
||||
// Admin
|
||||
r.mux.HandleFunc("/api/v1/admin/dashboard", r.authMW(r.adminMW(r.adminDashboardHandler)))
|
||||
r.mux.HandleFunc("/api/v1/admin/llm-calls", r.authMW(r.adminMW(r.llmCallsHandler)))
|
||||
r.mux.HandleFunc("/api/v1/admin/system", r.authMW(r.adminMW(r.systemInfoHandler)))
|
||||
r.mux.HandleFunc("/api/v1/admin/plugins", r.authMW(r.adminMW(r.pluginListHandler)))
|
||||
r.mux.HandleFunc("/api/v1/admin/sessions", r.authMW(r.adminMW(r.adminSessionsHandler)))
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
@@ -348,6 +361,40 @@ func (r *Router) adminDashboardHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// --- Admin extra handlers ---
|
||||
|
||||
func (r *Router) llmCallsHandler(w http.ResponseWriter, req *http.Request) {
|
||||
if r.deps.LLMLogger == nil {
|
||||
writeJSON(w, 200, M{"calls": []any{}, "stats": M{}})
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, M{
|
||||
"calls": r.deps.LLMLogger.Recent(50),
|
||||
"stats": r.deps.LLMLogger.Stats(),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Router) systemInfoHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, 200, M{
|
||||
"system": M{
|
||||
"version": "2.0.0-dev",
|
||||
"go_version": "1.26",
|
||||
"uptime_sec": int(time.Since(startTime).Seconds()),
|
||||
"ws_clients": r.deps.WSHub.Count(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Router) pluginListHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
plugins := r.deps.PluginList
|
||||
if plugins == nil { plugins = []map[string]string{} }
|
||||
writeJSON(w, 200, M{"plugins": plugins})
|
||||
}
|
||||
|
||||
func (r *Router) adminSessionsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, 200, M{"sessions": []any{}, "total": r.deps.WSHub.Count()})
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
type M = map[string]any
|
||||
|
||||
Reference in new issue
Block a user