summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrail Finder <wohilas@gmail.com>2025-12-08 15:38:52 +0300
committerGrail Finder <wohilas@gmail.com>2025-12-08 15:38:52 +0300
commit9b2fee37ab376619f6b0028f4c9aecf71204d44a (patch)
tree3d43d39c9cb25b855e9d58406328f07e5813856f
parent02bf308452aa127e9f3d2ce5b4821ba426c4c94a (diff)
Fix: user messages copied without content
-rw-r--r--Makefile2
-rw-r--r--bot.go33
-rw-r--r--bot_test.go155
-rw-r--r--extra/tts.go14
-rw-r--r--llm.go6
5 files changed, 200 insertions, 10 deletions
diff --git a/Makefile b/Makefile
index 6f0c7bb..b845e6f 100644
--- a/Makefile
+++ b/Makefile
@@ -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)
diff --git a/bot.go b/bot.go
index 6c1098e..0001afa 100644
--- a/bot.go
+++ b/bot.go
@@ -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 {
diff --git a/llm.go b/llm.go
index 38d6c22..599eb4e 100644
--- a/llm.go
+++ b/llm.go
@@ -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"
}
}