Skip to content

Commit

Permalink
Combine consecutive messages by the same user when talking to the Ant…
Browse files Browse the repository at this point in the history
…hropic API

Fixes #13
  • Loading branch information
spantaleev committed Sep 22, 2024
1 parent d4ddd29 commit 8b12bdf
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/agent/provider/anthropic/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ impl ControllerTrait for Controller {
})
};

// Avoid the situation where multiple user or assistant messages are sent consecutively,
// to avoid errors like:
// > API error: Error response: error Api error: invalid_request_error messages: roles must alternate between "user" and "assistant", but found multiple "user" roles in a row
// as reported here: https://github.com/etkecc/baibot/issues/13
//
// As https://docs.anthropic.com/en/api/messages says:
// > Our models are trained to operate on alternating user and assistant conversational turns.
let conversation = conversation.combine_consecutive_messages();

let mut conversation_messages = conversation.messages;

if params.context_management_enabled {
Expand Down
91 changes: 91 additions & 0 deletions src/conversation/llm/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,94 @@ pub struct Message {
pub struct Conversation {
pub messages: Vec<Message>,
}

impl Conversation {
/// Combine consecutive messages by the same author into a single message.
///
/// Certain models (like Anthropic) cannot tolerate consecutive messages by the same author,
/// so combining them helps avoid issues.
/// See: https://github.com/etkecc/baibot/issues/13
pub fn combine_consecutive_messages(&self) -> Conversation {
// We'll likely get fewer messages, but let's reserve the maximum we expect.
let mut new_messages = Vec::with_capacity(self.messages.len());
let mut last_seen_author: Option<Author> = None;

for message in &self.messages {
let Some(last_seen_author_clone) = last_seen_author.clone() else {
last_seen_author = Some(message.author.clone());
new_messages.push(message.clone());
continue;
};

if message.author != last_seen_author_clone {
last_seen_author = Some(message.author.clone());
new_messages.push(message.clone());
continue;
}

new_messages.last_mut().unwrap().message_text.push('\n');
new_messages
.last_mut()
.unwrap()
.message_text
.push_str(&message.message_text);
}

Conversation {
messages: new_messages,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn combine_consecutive_messages() {
let conversation = Conversation {
messages: vec![
Message {
author: Author::User,
message_text: "Hello".to_string(),
},
Message {
author: Author::User,
message_text: "How are you?".to_string(),
},
Message {
author: Author::User,
message_text: "I'm OK, btw.".to_string(),
},
Message {
author: Author::Assistant,
message_text: "Hi there!".to_string(),
},
Message {
author: Author::Assistant,
message_text: "I'm doing well, thank you.".to_string(),
},
Message {
author: Author::User,
message_text: "That's great!".to_string(),
},
],
};

let conversation = conversation.combine_consecutive_messages();

assert_eq!(conversation.messages.len(), 3);
assert_eq!(conversation.messages[0].author, Author::User);
assert_eq!(
conversation.messages[0].message_text,
"Hello\nHow are you?\nI'm OK, btw."
);
assert_eq!(conversation.messages[1].author, Author::Assistant);
assert_eq!(
conversation.messages[1].message_text,
"Hi there!\nI'm doing well, thank you."
);
assert_eq!(conversation.messages[2].author, Author::User);
assert_eq!(conversation.messages[2].message_text, "That's great!");
}
}

0 comments on commit 8b12bdf

Please sign in to comment.