use crate::{Degradations, Error, Message, Result, Subject};
pub fn retain_turns<T>(
turns: Vec<T>,
degradations: &mut Degradations,
turn: impl Fn(&T) -> EncodedTurn,
) -> Result<Vec<T>> {
if turns.is_empty() {
return Ok(turns);
}
let total = turns.len();
let mut kept: Vec<(usize, T)> = turns
.into_iter()
.enumerate()
.filter(|(_, item)| !turn(item).is_skipped())
.collect();
while let Some((position, item)) = kept.last() {
let encoded = turn(item);
if encoded.role != Role::Assistant || *position + 1 == total {
break;
}
degradations.dropped(
Subject::Content {
message: encoded.message,
block: None,
},
"the turns after the assistant turn were empty or dropped, and sending it last \
would ask the model to continue it; it was dropped",
);
kept.pop();
}
if kept.is_empty() {
return Err(Error::invalid_request(
"nothing to send: every message of the conversation is empty or was dropped",
)
.authored()
.with_provider(degradations.provider().to_owned()));
}
Ok(kept.into_iter().map(|(_, item)| item).collect())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EncodedTurn {
message: usize,
role: Role,
empty: bool,
}
impl EncodedTurn {
#[must_use]
pub fn new(index: usize, message: &Message, encoded_empty: bool) -> Self {
let empty = encoded_empty || message.is_empty();
match message {
Message::User(_) => Self::user(index, empty),
Message::Assistant(_) => Self::assistant(index, empty),
Message::Tool(_) => Self::tool(index),
}
}
#[must_use]
pub fn user(message: usize, empty: bool) -> Self {
Self {
message,
role: Role::User,
empty,
}
}
#[must_use]
pub fn assistant(message: usize, empty: bool) -> Self {
Self {
message,
role: Role::Assistant,
empty,
}
}
#[must_use]
pub fn tool(message: usize) -> Self {
Self {
message,
role: Role::Tool,
empty: false,
}
}
#[must_use]
pub fn message(&self) -> usize {
self.message
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.empty
}
fn is_skipped(self) -> bool {
self.empty && self.role != Role::Tool
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Role {
User,
Assistant,
Tool,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ContentPart, UnsupportedPolicy, WarningKind};
fn run(
turns: &[EncodedTurn],
policy: UnsupportedPolicy,
) -> (Result<Vec<usize>>, Vec<crate::Warning>) {
let mut degradations = Degradations::new("demo", policy);
let kept = retain_turns(turns.to_vec(), &mut degradations, |turn| *turn)
.map(|kept| kept.iter().map(EncodedTurn::message).collect());
(kept, degradations.into_warnings())
}
fn dropped(message: usize) -> Subject {
Subject::Content {
message,
block: None,
}
}
#[test]
fn empty_user_and_assistant_turns_are_skipped_without_a_warning() {
let turns = [
EncodedTurn::user(0, false),
EncodedTurn::assistant(1, true),
EncodedTurn::assistant(2, false),
EncodedTurn::user(3, true),
EncodedTurn::user(4, false),
];
let (kept, warnings) = run(&turns, UnsupportedPolicy::Warn);
assert_eq!(kept.unwrap(), [0, 2, 4]);
assert!(warnings.is_empty(), "{warnings:?}");
}
#[test]
fn a_tool_turn_is_never_skipped() {
let turns = [
EncodedTurn::user(0, false),
EncodedTurn::assistant(1, false),
EncodedTurn::tool(2),
];
let (kept, warnings) = run(&turns, UnsupportedPolicy::Warn);
assert_eq!(kept.unwrap(), [0, 1, 2]);
assert!(warnings.is_empty());
assert!(!EncodedTurn::tool(2).is_empty());
}
#[test]
fn an_assistant_turn_left_last_is_dropped_under_every_policy() {
for policy in [UnsupportedPolicy::Warn, UnsupportedPolicy::Error] {
let turns = [
EncodedTurn::user(0, false),
EncodedTurn::assistant(1, false),
EncodedTurn::user(2, true),
];
let (kept, warnings) = run(&turns, policy);
assert_eq!(kept.unwrap(), [0], "{policy:?}");
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert_eq!(warnings[0].kind, WarningKind::Dropped);
assert_eq!(warnings[0].subject, dropped(1));
assert_eq!(warnings[0].provider.as_deref(), Some("demo"));
}
}
#[test]
fn trailing_drops_repeat_until_a_turn_that_may_end_the_conversation() {
let turns = [
EncodedTurn::user(0, false),
EncodedTurn::assistant(1, false),
EncodedTurn::tool(2),
EncodedTurn::assistant(3, false),
EncodedTurn::assistant(4, false),
EncodedTurn::assistant(5, true),
EncodedTurn::user(6, true),
];
let (kept, warnings) = run(&turns, UnsupportedPolicy::Warn);
assert_eq!(kept.unwrap(), [0, 1, 2]);
let subjects: Vec<_> = warnings.into_iter().map(|w| w.subject).collect();
assert_eq!(subjects, [dropped(4), dropped(3)]);
}
#[test]
fn a_prefill_the_caller_sent_last_is_kept() {
let turns = [
EncodedTurn::user(0, false),
EncodedTurn::assistant(1, false),
];
let (kept, warnings) = run(&turns, UnsupportedPolicy::Error);
assert_eq!(kept.unwrap(), [0, 1]);
assert!(warnings.is_empty());
}
#[test]
fn several_turns_may_come_from_one_message() {
let turns = [
EncodedTurn::user(0, false),
EncodedTurn::assistant(1, false),
EncodedTurn::tool(2),
EncodedTurn::user(2, true),
];
let (kept, warnings) = run(&turns, UnsupportedPolicy::Warn);
assert_eq!(kept.unwrap(), [0, 1, 2]);
assert!(warnings.is_empty());
}
#[test]
fn nothing_left_to_send_is_an_authored_invalid_request() {
let turns = [EncodedTurn::user(0, true), EncodedTurn::assistant(1, true)];
let (kept, warnings) = run(&turns, UnsupportedPolicy::Warn);
let err = kept.unwrap_err();
assert!(matches!(err, Error::InvalidRequest { .. }), "{err}");
assert_eq!(err.message_origin(), crate::MessageOrigin::Authored);
assert_eq!(
err.context().and_then(|c| c.provider.as_deref()),
Some("demo")
);
assert!(warnings.is_empty());
let turns = [EncodedTurn::assistant(0, false), EncodedTurn::user(1, true)];
let (kept, warnings) = run(&turns, UnsupportedPolicy::Warn);
assert!(matches!(kept, Err(Error::InvalidRequest { .. })));
assert_eq!(warnings.len(), 1);
}
#[test]
fn a_request_without_turns_is_left_to_the_provider() {
let (kept, warnings) = run(&[], UnsupportedPolicy::Warn);
assert!(kept.unwrap().is_empty());
assert!(warnings.is_empty());
}
#[test]
fn turns_from_messages_combine_the_encoding_and_the_message() {
let empty_user = Message::user("");
let empty_parts = Message::user_parts(vec![]);
let reply = Message::assistant("4");
let empty_reply = Message::assistant_blocks(vec![]);
let empty_result = Message::tool_result("c1", "f", "");
assert!(EncodedTurn::new(0, &empty_user, false).is_empty());
assert!(EncodedTurn::new(0, &empty_parts, false).is_empty());
assert!(EncodedTurn::new(0, &empty_reply, false).is_empty());
assert!(!EncodedTurn::new(0, &reply, false).is_empty());
assert!(EncodedTurn::new(0, &reply, true).is_empty());
assert!(!EncodedTurn::new(0, &empty_result, true).is_empty());
let foreign = Message::user_parts(vec![ContentPart::Other(
crate::OtherContent::new("x", Default::default()).with_origin("other"),
)]);
assert!(!EncodedTurn::new(0, &foreign, false).is_empty());
assert!(EncodedTurn::new(0, &foreign, true).is_empty());
}
}