Fix: user messages copied without content
This commit is contained in:
2
Makefile
2
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)
|
||||
|
||||
33
bot.go
33
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
|
||||
|
||||
155
bot_test.go
Normal file
155
bot_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
14
extra/tts.go
14
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 {
|
||||
|
||||
6
llm.go
6
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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user