diff options
| author | Grail Finder <wohilas@gmail.com> | 2025-12-08 15:38:52 +0300 |
|---|---|---|
| committer | Grail Finder <wohilas@gmail.com> | 2025-12-08 15:38:52 +0300 |
| commit | 9b2fee37ab376619f6b0028f4c9aecf71204d44a (patch) | |
| tree | 3d43d39c9cb25b855e9d58406328f07e5813856f | |
| parent | 02bf308452aa127e9f3d2ce5b4821ba426c4c94a (diff) | |
Fix: user messages copied without content
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | bot.go | 33 | ||||
| -rw-r--r-- | bot_test.go | 155 | ||||
| -rw-r--r-- | extra/tts.go | 14 | ||||
| -rw-r--r-- | llm.go | 6 |
5 files changed, 200 insertions, 10 deletions
@@ -30,7 +30,7 @@ download-whisper-model: ## Download Whisper model for STT in batteries directory echo "Please run 'make setup-whisper' first to clone the repository."; \ exit 1; \ fi - @cd batteries/whisper.cpp && make large-v3-turbo + @cd batteries/whisper.cpp && bash ./models/download-ggml-model.sh large-v3-turbo-q5_0 @echo "Whisper model downloaded successfully!" # Docker targets for STT/TTS services (in batteries directory) @@ -74,6 +74,9 @@ func cleanNullMessages(messages []models.RoleMsg) []models.RoleMsg { // Include message if it has content or if it's a tool response (which might have tool_call_id) if msg.HasContent() || msg.ToolCallID != "" { cleaned = append(cleaned, msg) + } else { + // Log filtered messages for debugging + logger.Warn("filtering out message during cleaning", "role", msg.Role, "content", msg.Content, "tool_call_id", msg.ToolCallID, "has_content", msg.HasContent()) } } return consolidateConsecutiveAssistantMessages(cleaned) @@ -103,9 +106,12 @@ func consolidateConsecutiveAssistantMessages(messages []models.RoleMsg) []models if currentAssistantMsg.IsContentParts() || msg.IsContentParts() { // Handle structured content if !currentAssistantMsg.IsContentParts() { + // Preserve the original ToolCallID before conversion + originalToolCallID := currentAssistantMsg.ToolCallID // Convert existing content to content parts currentAssistantMsg = models.NewMultimodalMsg(currentAssistantMsg.Role, []interface{}{models.TextContentPart{Type: "text", Text: currentAssistantMsg.Content}}) - currentAssistantMsg.ToolCallID = msg.ToolCallID + // Restore the original ToolCallID to preserve tool call linking + currentAssistantMsg.ToolCallID = originalToolCallID } if msg.IsContentParts() { currentAssistantMsg.ContentParts = append(currentAssistantMsg.ContentParts, msg.GetContentParts()...) @@ -119,6 +125,7 @@ func consolidateConsecutiveAssistantMessages(messages []models.RoleMsg) []models } else { currentAssistantMsg.Content = msg.Content } + // ToolCallID is already preserved since we're not creating a new message object when just concatenating content } } } else { @@ -556,9 +563,19 @@ out: }) } + logger.Debug("chatRound: before cleanChatBody", "messages_before_clean", len(chatBody.Messages)) + for i, msg := range chatBody.Messages { + logger.Debug("chatRound: before cleaning", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) + } + // Clean null/empty messages to prevent API issues with endpoints like llama.cpp jinja template cleanChatBody() + logger.Debug("chatRound: after cleanChatBody", "messages_after_clean", len(chatBody.Messages)) + for i, msg := range chatBody.Messages { + logger.Debug("chatRound: after cleaning", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) + } + colorText() updateStatusLine() // bot msg is done; @@ -574,8 +591,17 @@ out: func cleanChatBody() { if chatBody != nil && chatBody.Messages != nil { originalLen := len(chatBody.Messages) + logger.Debug("cleanChatBody: before cleaning", "message_count", originalLen) + for i, msg := range chatBody.Messages { + logger.Debug("cleanChatBody: before clean", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) + } + chatBody.Messages = cleanNullMessages(chatBody.Messages) - logger.Debug("cleaned chat body", "original_len", originalLen, "new_len", len(chatBody.Messages)) + + logger.Debug("cleanChatBody: after cleaning", "original_len", originalLen, "new_len", len(chatBody.Messages)) + for i, msg := range chatBody.Messages { + logger.Debug("cleanChatBody: after clean", "index", i, "role", msg.Role, "content_len", len(msg.Content), "has_content", msg.HasContent(), "tool_call_id", msg.ToolCallID) + } } } @@ -621,6 +647,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { Content: fmt.Sprintf("Error processing tool call: %v. Please check the JSON format and try again.", err), } chatBody.Messages = append(chatBody.Messages, toolResponseMsg) + logger.Debug("findCall: added tool error response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "message_count_after_add", len(chatBody.Messages)) // Trigger the assistant to continue processing with the error message chatRound("", cfg.AssistantRole, tv, false, false) return @@ -637,6 +664,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { ToolCallID: lastToolCallID, // Use the stored tool call ID } chatBody.Messages = append(chatBody.Messages, toolResponseMsg) + logger.Debug("findCall: added tool not implemented response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", len(chatBody.Messages)) // Clear the stored tool call ID after using it lastToolCallID = "" @@ -657,6 +685,7 @@ func findCall(msg, toolCall string, tv *tview.TextView) { ToolCallID: lastToolCallID, // Use the stored tool call ID } chatBody.Messages = append(chatBody.Messages, toolResponseMsg) + logger.Debug("findCall: added actual tool response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", len(chatBody.Messages)) // Clear the stored tool call ID after using it lastToolCallID = "" // Trigger the assistant to continue processing with the new tool response diff --git a/bot_test.go b/bot_test.go new file mode 100644 index 0000000..2d59c3c --- /dev/null +++ b/bot_test.go @@ -0,0 +1,155 @@ +package main + +import ( + "gf-lt/config" + "gf-lt/models" + "reflect" + "testing" +) + +func TestConsolidateConsecutiveAssistantMessages(t *testing.T) { + // Mock config for testing + testCfg := &config.Config{ + AssistantRole: "assistant", + WriteNextMsgAsCompletionAgent: "", + } + cfg = testCfg + + tests := []struct { + name string + input []models.RoleMsg + expected []models.RoleMsg + }{ + { + name: "no consecutive assistant messages", + input: []models.RoleMsg{ + {Role: "user", Content: "Hello"}, + {Role: "assistant", Content: "Hi there"}, + {Role: "user", Content: "How are you?"}, + }, + expected: []models.RoleMsg{ + {Role: "user", Content: "Hello"}, + {Role: "assistant", Content: "Hi there"}, + {Role: "user", Content: "How are you?"}, + }, + }, + { + name: "consecutive assistant messages should be consolidated", + input: []models.RoleMsg{ + {Role: "user", Content: "Hello"}, + {Role: "assistant", Content: "First part"}, + {Role: "assistant", Content: "Second part"}, + {Role: "user", Content: "Thanks"}, + }, + expected: []models.RoleMsg{ + {Role: "user", Content: "Hello"}, + {Role: "assistant", Content: "First part\nSecond part"}, + {Role: "user", Content: "Thanks"}, + }, + }, + { + name: "multiple sets of consecutive assistant messages", + input: []models.RoleMsg{ + {Role: "user", Content: "First question"}, + {Role: "assistant", Content: "First answer part 1"}, + {Role: "assistant", Content: "First answer part 2"}, + {Role: "user", Content: "Second question"}, + {Role: "assistant", Content: "Second answer part 1"}, + {Role: "assistant", Content: "Second answer part 2"}, + {Role: "assistant", Content: "Second answer part 3"}, + }, + expected: []models.RoleMsg{ + {Role: "user", Content: "First question"}, + {Role: "assistant", Content: "First answer part 1\nFirst answer part 2"}, + {Role: "user", Content: "Second question"}, + {Role: "assistant", Content: "Second answer part 1\nSecond answer part 2\nSecond answer part 3"}, + }, + }, + { + name: "single assistant message (no consolidation needed)", + input: []models.RoleMsg{ + {Role: "user", Content: "Hello"}, + {Role: "assistant", Content: "Hi there"}, + }, + expected: []models.RoleMsg{ + {Role: "user", Content: "Hello"}, + {Role: "assistant", Content: "Hi there"}, + }, + }, + { + name: "only assistant messages", + input: []models.RoleMsg{ + {Role: "assistant", Content: "First"}, + {Role: "assistant", Content: "Second"}, + {Role: "assistant", Content: "Third"}, + }, + expected: []models.RoleMsg{ + {Role: "assistant", Content: "First\nSecond\nThird"}, + }, + }, + { + name: "user messages at the end are preserved", + input: []models.RoleMsg{ + {Role: "assistant", Content: "First"}, + {Role: "assistant", Content: "Second"}, + {Role: "user", Content: "Final user message"}, + }, + expected: []models.RoleMsg{ + {Role: "assistant", Content: "First\nSecond"}, + {Role: "user", Content: "Final user message"}, + }, + }, + { + name: "tool call ids preserved in consolidation", + input: []models.RoleMsg{ + {Role: "user", Content: "Hello"}, + {Role: "assistant", Content: "First part", ToolCallID: "call_123"}, + {Role: "assistant", Content: "Second part", ToolCallID: "call_123"}, // Same ID + {Role: "user", Content: "Thanks"}, + }, + expected: []models.RoleMsg{ + {Role: "user", Content: "Hello"}, + {Role: "assistant", Content: "First part\nSecond part", ToolCallID: "call_123"}, + {Role: "user", Content: "Thanks"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := consolidateConsecutiveAssistantMessages(tt.input) + + if len(result) != len(tt.expected) { + t.Errorf("Expected %d messages, got %d", len(tt.expected), len(result)) + t.Logf("Result: %+v", result) + t.Logf("Expected: %+v", tt.expected) + return + } + + for i, expectedMsg := range tt.expected { + if i >= len(result) { + t.Errorf("Result has fewer messages than expected at index %d", i) + continue + } + + actualMsg := result[i] + if actualMsg.Role != expectedMsg.Role { + t.Errorf("Message %d: expected role '%s', got '%s'", i, expectedMsg.Role, actualMsg.Role) + } + + if actualMsg.Content != expectedMsg.Content { + t.Errorf("Message %d: expected content '%s', got '%s'", i, expectedMsg.Content, actualMsg.Content) + } + + if actualMsg.ToolCallID != expectedMsg.ToolCallID { + t.Errorf("Message %d: expected ToolCallID '%s', got '%s'", i, expectedMsg.ToolCallID, actualMsg.ToolCallID) + } + } + + // Additional check: ensure no messages were lost + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("Result does not match expected:\nResult: %+v\nExpected: %+v", result, tt.expected) + } + }) + } +}
\ No newline at end of file diff --git a/extra/tts.go b/extra/tts.go index 31e6887..c6f373a 100644 --- a/extra/tts.go +++ b/extra/tts.go @@ -48,7 +48,7 @@ type KokoroOrator struct { func (o *KokoroOrator) stoproutine() { <-TTSDoneChan - o.logger.Info("orator got done signal") + o.logger.Debug("orator got done signal") o.Stop() // drain the channel for len(TTSTextChan) > 0 { @@ -72,7 +72,7 @@ func (o *KokoroOrator) readroutine() { } text := o.textBuffer.String() sentences := tokenizer.Tokenize(text) - o.logger.Info("adding chunk", "chunk", chunk, "text", text, "sen-len", len(sentences)) + o.logger.Debug("adding chunk", "chunk", chunk, "text", text, "sen-len", len(sentences)) for i, sentence := range sentences { if i == len(sentences)-1 { // last sentence o.textBuffer.Reset() @@ -83,13 +83,13 @@ func (o *KokoroOrator) readroutine() { } continue // if only one (often incomplete) sentence; wait for next chunk } - o.logger.Info("calling Speak with sentence", "sent", sentence.Text) + o.logger.Debug("calling Speak with sentence", "sent", sentence.Text) if err := o.Speak(sentence.Text); err != nil { o.logger.Error("tts failed", "sentence", sentence.Text, "error", err) } } case <-TTSFlushChan: - o.logger.Info("got flushchan signal start") + o.logger.Debug("got flushchan signal start") // lln is done get the whole message out if len(TTSTextChan) > 0 { // otherwise might get stuck for chunk := range TTSTextChan { @@ -110,7 +110,7 @@ func (o *KokoroOrator) readroutine() { remaining := o.textBuffer.String() o.textBuffer.Reset() if remaining != "" { - o.logger.Info("calling Speak with remainder", "rem", remaining) + o.logger.Debug("calling Speak with remainder", "rem", remaining) if err := o.Speak(remaining); err != nil { o.logger.Error("tts failed", "sentence", remaining, "error", err) } @@ -171,7 +171,7 @@ func (o *KokoroOrator) requestSound(text string) (io.ReadCloser, error) { } func (o *KokoroOrator) Speak(text string) error { - o.logger.Info("fn: Speak is called", "text-len", len(text)) + o.logger.Debug("fn: Speak is called", "text-len", len(text)) body, err := o.requestSound(text) if err != nil { o.logger.Error("request failed", "error", err) @@ -202,7 +202,7 @@ func (o *KokoroOrator) Speak(text string) error { func (o *KokoroOrator) Stop() { // speaker.Clear() - o.logger.Info("attempted to stop orator", "orator", o) + o.logger.Debug("attempted to stop orator", "orator", o) speaker.Lock() defer speaker.Unlock() if o.currentStream != nil { @@ -211,6 +211,8 @@ func (op LCPChat) FormMsg(msg, role string, resume bool) (io.Reader, error) { newMsg = models.NewRoleMsg(role, msg) } chatBody.Messages = append(chatBody.Messages, newMsg) + logger.Debug("LCPChat FormMsg: added message to chatBody", "role", newMsg.Role, "content_len", len(newMsg.Content), "message_count_after_add", len(chatBody.Messages)) + // if rag - add as system message to avoid conflicts with tool usage if cfg.RAGEnabled { ragResp, err := chatRagUse(newMsg.Content) @@ -221,6 +223,7 @@ func (op LCPChat) FormMsg(msg, role string, resume bool) (io.Reader, error) { // Use system role for RAG context to avoid conflicts with tool usage ragMsg := models.RoleMsg{Role: "system", Content: RAGMsg + ragResp} chatBody.Messages = append(chatBody.Messages, ragMsg) + logger.Debug("LCPChat FormMsg: added RAG message to chatBody", "role", ragMsg.Role, "rag_content_len", len(ragMsg.Content), "message_count_after_rag", len(chatBody.Messages)) } } // openai /v1/chat does not support custom roles; needs to be user, assistant, system @@ -231,6 +234,7 @@ func (op LCPChat) FormMsg(msg, role string, resume bool) (io.Reader, error) { } for i, msg := range chatBody.Messages { if msg.Role == cfg.UserRole { + bodyCopy.Messages[i] = msg bodyCopy.Messages[i].Role = "user" } else { bodyCopy.Messages[i] = msg @@ -382,6 +386,7 @@ func (ds DeepSeekerChat) FormMsg(msg, role string, resume bool) (io.Reader, erro } for i, msg := range chatBody.Messages { if msg.Role == cfg.UserRole || i == 1 { + bodyCopy.Messages[i] = msg bodyCopy.Messages[i].Role = "user" } else { bodyCopy.Messages[i] = msg @@ -559,6 +564,7 @@ func (or OpenRouterChat) FormMsg(msg, role string, resume bool) (io.Reader, erro bodyCopy.Messages[i] = msg // Standardize role if it's a user role if bodyCopy.Messages[i].Role == cfg.UserRole { + bodyCopy.Messages[i] = msg bodyCopy.Messages[i].Role = "user" } } |
