@@ -14,8 +14,10 @@ import (
1414)
1515
1616const (
17- conversationMetadataMessageMaxTokens = int64 (5000 )
18- conversationMetadataTitlePrompt = `Generate a concise title from the first conversation turn below. Return ONLY a valid JSON object.
17+ conversationMetadataMessageMaxTokens = int64 (5000 )
18+ conversationFirstMessageTitleMaxRunes = 20
19+ conversationAutoGenerateTitleSettingKey = "chat.auto_generate_title"
20+ conversationMetadataTitlePrompt = `Generate a concise title from the first conversation turn below. Return ONLY a valid JSON object.
1921
2022## Constraints
21231. **Content**: Reflect the primary topic, goal, or main subject.
@@ -72,9 +74,6 @@ func (s *Service) maybeGenerateConversationMetadataAsync(conversation model.Conv
7274}
7375
7476func (s * Service ) generateConversationMetadata (ctx context.Context , conversation model.Conversation , userMsg model.Message , assistantMsg model.Message ) (* model.Conversation , error ) {
75- if s .routeResolver == nil || s .llmClient == nil {
76- return nil , nil
77- }
7877 cfg := s .cfg .Snapshot ()
7978 messages := buildConversationMetadataMessages (userMsg , assistantMsg )
8079
@@ -95,7 +94,13 @@ func (s *Service) generateConversationMetadata(ctx context.Context, conversation
9594 }
9695 }
9796
98- if shouldAutoReplaceConversationTitle (conversation .Title ) {
97+ shouldReplaceTitle := shouldAutoReplaceConversationTitle (conversation .Title )
98+ shouldGenerateTitle := shouldReplaceTitle && s .autoGenerateConversationTitleEnabled (ctx , conversation .UserID )
99+ if shouldReplaceTitle && ! shouldGenerateTitle {
100+ title = conversationTitleFromFirstUserMessage (userMsg .Content )
101+ }
102+
103+ if s .routeResolver != nil && s .llmClient != nil && shouldGenerateTitle {
99104 wg .Add (1 )
100105 go func () {
101106 defer wg .Done ()
@@ -112,29 +117,31 @@ func (s *Service) generateConversationMetadata(ctx context.Context, conversation
112117 }()
113118 }
114119
115- wg .Add (1 )
116- go func () {
117- defer wg .Done ()
118- labelsPrompt := renderConversationMetadataPrompt (cfg .ConversationLabelsPrompt , conversationMetadataLabelsPrompt , messages )
119- labelsOut , err := s .callConversationMetadataLLM (ctx , cfg .ConversationTaskModel , conversation .Model , conversation .UserID , conversation .ID , labelsPrompt )
120- if err != nil {
121- setGenerateErr (err )
122- return
123- }
124- s .recordBasicServiceUsage (ctx , conversation .UserID , conversation .ID , "labels" , "标签" , labelsOut .PlatformModelName , labelsOut .RoutedBindingCode , labelsOut .ProviderProtocol , labelsOut .UpstreamName , labelsOut .UpstreamModel , "5m" , labelsOut .Usage , labelsOut .Messages , labelsOut .Text , labelsOut .LatencyMS )
125- labels := sanitizeGeneratedConversationLabels (parseGeneratedConversationLabels (labelsOut .Text ))
126- if len (labels ) == 0 {
127- return
128- }
129- raw , marshalErr := json .Marshal (labels )
130- if marshalErr != nil {
131- setGenerateErr (marshalErr )
132- return
133- }
134- mu .Lock ()
135- labelsJSON = string (raw )
136- mu .Unlock ()
137- }()
120+ if s .routeResolver != nil && s .llmClient != nil {
121+ wg .Add (1 )
122+ go func () {
123+ defer wg .Done ()
124+ labelsPrompt := renderConversationMetadataPrompt (cfg .ConversationLabelsPrompt , conversationMetadataLabelsPrompt , messages )
125+ labelsOut , err := s .callConversationMetadataLLM (ctx , cfg .ConversationTaskModel , conversation .Model , conversation .UserID , conversation .ID , labelsPrompt )
126+ if err != nil {
127+ setGenerateErr (err )
128+ return
129+ }
130+ s .recordBasicServiceUsage (ctx , conversation .UserID , conversation .ID , "labels" , "标签" , labelsOut .PlatformModelName , labelsOut .RoutedBindingCode , labelsOut .ProviderProtocol , labelsOut .UpstreamName , labelsOut .UpstreamModel , "5m" , labelsOut .Usage , labelsOut .Messages , labelsOut .Text , labelsOut .LatencyMS )
131+ labels := sanitizeGeneratedConversationLabels (parseGeneratedConversationLabels (labelsOut .Text ))
132+ if len (labels ) == 0 {
133+ return
134+ }
135+ raw , marshalErr := json .Marshal (labels )
136+ if marshalErr != nil {
137+ setGenerateErr (marshalErr )
138+ return
139+ }
140+ mu .Lock ()
141+ labelsJSON = string (raw )
142+ mu .Unlock ()
143+ }()
144+ }
138145
139146 wg .Wait ()
140147 mu .Lock ()
@@ -388,6 +395,19 @@ func sanitizeGeneratedConversationTitle(raw string) string {
388395 return strings .TrimSpace (value )
389396}
390397
398+ func conversationTitleFromFirstUserMessage (content string ) string {
399+ value := strings .Join (strings .Fields (strings .TrimSpace (content )), " " )
400+ value = strings .Trim (value , " \t \r \n \" '`“”‘’" )
401+ if value == "" {
402+ return ""
403+ }
404+ runes := []rune (value )
405+ if len (runes ) > conversationFirstMessageTitleMaxRunes {
406+ value = string (runes [:conversationFirstMessageTitleMaxRunes ])
407+ }
408+ return strings .TrimSpace (value )
409+ }
410+
391411func sanitizeGeneratedConversationLabels (raw []string ) []string {
392412 seen := make (map [string ]struct {}, len (raw ))
393413 labels := make ([]string , 0 , len (raw ))
@@ -414,6 +434,17 @@ func sanitizeGeneratedConversationLabels(raw []string) []string {
414434 return labels
415435}
416436
437+ func (s * Service ) autoGenerateConversationTitleEnabled (ctx context.Context , userID uint ) bool {
438+ value , err := s .repo .GetUserSettingValue (ctx , userID , conversationAutoGenerateTitleSettingKey )
439+ if err != nil {
440+ if s .logger != nil {
441+ s .logger .Warn ("conversation_title_setting_load_failed" , zap .Uint ("user_id" , userID ), zap .Error (err ))
442+ }
443+ return true
444+ }
445+ return strings .TrimSpace (strings .ToLower (value )) != "false"
446+ }
447+
417448func shouldAutoReplaceConversationTitle (title string ) bool {
418449 value := strings .TrimSpace (strings .ToLower (title ))
419450 switch value {
0 commit comments