mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 04:16:06 +00:00
feat(chat): auto-generate conversation titles via lightweight LLM call
After the first agent run completes, generate a short title from the user's message using a 10s/50-token LLM call. Title is stored in the session label column and pushed to clients via session.updated event.
This commit is contained in:
1 parent
24ad97af31
commit
4da85913a2
7 files changed
+94
-4
No files matched your search
@@ -16,7 +16,7 @@ func registerAllMethods(server *gateway.Server, agents *agent.Router, sessStore
|
||||
router := server.Router()
|
||||
|
||||
// Phase 1: Core methods
|
||||
methods.NewChatMethods(agents, sessStore, server.RateLimiter()).Register(router)
|
||||
methods.NewChatMethods(agents, sessStore, server.RateLimiter(), msgBus).Register(router)
|
||||
methods.NewAgentsMethods(agents, cfg, cfgPath, workspace, agentStore, contextFileInterceptor, msgBus).Register(router)
|
||||
methods.NewSessionsMethods(sessStore, msgBus).Register(router)
|
||||
methods.NewConfigMethods(cfg, cfgPath, configSecretsStore, msgBus).Register(router)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
)
|
||||
|
||||
const titleGenerateTimeout = 10 * time.Second
|
||||
|
||||
const titleSystemPrompt = `Generate a short title (max 15 words) for this conversation based on the user's message. Reply with only the title, no quotes or punctuation wrapping.`
|
||||
|
||||
// GenerateTitle uses a lightweight LLM call to create a short conversation title
|
||||
// from the user's first message. Returns empty string on error.
|
||||
func GenerateTitle(ctx context.Context, provider providers.Provider, model, userMessage string) string {
|
||||
ctx, cancel := context.WithTimeout(ctx, titleGenerateTimeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := provider.Chat(ctx, providers.ChatRequest{
|
||||
Messages: []providers.Message{
|
||||
{Role: "system", Content: titleSystemPrompt},
|
||||
{Role: "user", Content: userMessage},
|
||||
},
|
||||
Model: model,
|
||||
Options: map[string]any{
|
||||
providers.OptMaxTokens: 50,
|
||||
providers.OptTemperature: 0.3,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("title generation failed", "error", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(resp.Content)
|
||||
// Strip surrounding quotes if present.
|
||||
title = strings.Trim(title, "\"'`")
|
||||
title = strings.TrimSpace(title)
|
||||
|
||||
if runes := []rune(title); len(runes) > 100 {
|
||||
title = string(runes[:100])
|
||||
}
|
||||
return title
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package agent
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
)
|
||||
|
||||
// Agent is the core abstraction for an AI agent execution loop.
|
||||
// Implemented by *Loop; extracted as an interface for testability and composability.
|
||||
@@ -10,4 +14,5 @@ type Agent interface {
|
||||
IsRunning() bool
|
||||
Model() string
|
||||
ProviderName() string
|
||||
Provider() providers.Provider
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"log/slog"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/channels/media"
|
||||
@@ -22,10 +24,11 @@ type ChatMethods struct {
|
||||
agents *agent.Router
|
||||
sessions store.SessionStore
|
||||
rateLimiter *gateway.RateLimiter
|
||||
eventBus bus.EventPublisher
|
||||
}
|
||||
|
||||
func NewChatMethods(agents *agent.Router, sess store.SessionStore, rl *gateway.RateLimiter) *ChatMethods {
|
||||
return &ChatMethods{agents: agents, sessions: sess, rateLimiter: rl}
|
||||
func NewChatMethods(agents *agent.Router, sess store.SessionStore, rl *gateway.RateLimiter, eventBus bus.EventPublisher) *ChatMethods {
|
||||
return &ChatMethods{agents: agents, sessions: sess, rateLimiter: rl, eventBus: eventBus}
|
||||
}
|
||||
|
||||
// Register adds chat methods to the router.
|
||||
@@ -206,6 +209,28 @@ func (m *ChatMethods) handleSend(ctx context.Context, client *gateway.Client, re
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-generate conversation title on first message (label empty = never titled).
|
||||
if label := m.sessions.GetLabel(sessionKey); label == "" {
|
||||
agentProvider := loop.Provider()
|
||||
agentModel := loop.Model()
|
||||
userMsg := params.Message
|
||||
go func() {
|
||||
title := agent.GenerateTitle(context.Background(), agentProvider, agentModel, userMsg)
|
||||
if title == "" {
|
||||
return
|
||||
}
|
||||
m.sessions.SetLabel(sessionKey, title)
|
||||
if err := m.sessions.Save(sessionKey); err != nil {
|
||||
slog.Warn("failed to save session title", "sessionKey", sessionKey, "error", err)
|
||||
return
|
||||
}
|
||||
m.eventBus.Broadcast(bus.Event{
|
||||
Name: protocol.EventSessionUpdated,
|
||||
Payload: map[string]string{"sessionKey": sessionKey, "label": title},
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"runId": result.RunID,
|
||||
"content": result.Content,
|
||||
|
||||
@@ -159,6 +159,15 @@ func (s *PGSessionStore) SetSummary(key, summary string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PGSessionStore) GetLabel(key string) string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if data, ok := s.cache[key]; ok {
|
||||
return data.Label
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *PGSessionStore) SetLabel(key, label string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -91,6 +91,7 @@ type SessionStore interface {
|
||||
GetHistory(key string) []providers.Message
|
||||
GetSummary(key string) string
|
||||
SetSummary(key, summary string)
|
||||
GetLabel(key string) string
|
||||
SetLabel(key, label string)
|
||||
SetAgentInfo(key string, agentUUID uuid.UUID, userID string)
|
||||
UpdateMetadata(key, model, provider, channel string)
|
||||
|
||||
@@ -88,6 +88,9 @@ const (
|
||||
// Audit log event (internal, not forwarded to WS clients).
|
||||
EventAuditLog = "audit.log"
|
||||
|
||||
// Session lifecycle events.
|
||||
EventSessionUpdated = "session.updated"
|
||||
|
||||
// Zalo Personal QR login events (client-scoped, not broadcast).
|
||||
EventZaloPersonalQRCode = "zalo.personal.qr.code"
|
||||
EventZaloPersonalQRDone = "zalo.personal.qr.done"
|
||||
|
||||
Reference in new issue
Block a user