diff --git a/crates/openplotva-llm/src/aifarm.rs b/crates/openplotva-llm/src/aifarm.rs index 9b3eb4a..58a9b47 100644 --- a/crates/openplotva-llm/src/aifarm.rs +++ b/crates/openplotva-llm/src/aifarm.rs @@ -5611,7 +5611,8 @@ const GENERATION_TOOL_NAMES: &[&str] = &["draw_image", "draw_api", "generate_son /// Review a final reply against the bot's latest replies in `history` and the current /// message: its reaction opener, read-back quotes and an unstarted generation promise are /// cut; an opening still copied from a recent reply, or a reply that is only a promise, -/// is re-sampled once per turn. Custom personas keep their own form. +/// is re-sampled once per turn. A custom persona may keep a fixed opening of its own, so +/// it is never re-sampled for one. fn review_reply_shape( input: &DialogInput, history: &[HistoryMessage], @@ -5619,9 +5620,7 @@ fn review_reply_shape( text: String, generation_offered: bool, ) -> Result { - if !input.persona.custom_persona.trim().is_empty() { - return Ok(text); - } + let custom_persona = !input.persona.custom_persona.trim().is_empty(); let recent_replies: Vec<&str> = history .iter() .rev() @@ -5636,7 +5635,7 @@ fn review_reply_shape( recent_replies: &recent_replies, user_text: &input.message.text, generation_pending: generation_offered && !generation_started(transcript), - resample_allowed: input.resample_verdict.trim().is_empty(), + resample_allowed: !custom_persona && input.resample_verdict.trim().is_empty(), }; match reply_shape::review_reply(&text, &context) { reply_shape::ReplyShape::Keep => Ok(text), @@ -10940,6 +10939,47 @@ mod tests { Ok(()) } + #[tokio::test] + async fn chat_step_trims_but_never_resamples_a_custom_persona_reply() + -> Result<(), CompletionError> { + for (reply, delivered) in [ + ( + "Ха-ха, ну ты и гурман! 😂 Смотрю: это сильный вкус.", + "Смотрю: это сильный вкус.", + ), + ( + "(я чувствую, как дождь стучит по крыше)", + "(я чувствую, как дождь стучит по крыше)", + ), + ] { + let (provider, _, _) = direct_dialog_provider( + json!({"choices": [{"message": {"role": "assistant", "content": reply}}]}), + AifarmDialogConfig::default(), + ); + let mut input = base_input(); + input.persona.custom_persona = "Ученица и помощник".to_owned(); + input + .history + .push(bot_history_reply(10, "(я чувствую, как ветер шумит)")); + let output = crate::ChatStepProvider::run_chat_step( + &provider, + ChatStepRequest { + required_tool: None, + preferred_target: None, + input, + transcript: Vec::new(), + tools: ToolsMode::Native( + native_tool_values(&[STEP_WEB_SEARCH]).expect("tool defs"), + ), + iteration: 1, + }, + ) + .await?; + assert_eq!(output.text, delivered); + } + Ok(()) + } + #[tokio::test] async fn chat_step_resamples_a_generation_announced_without_a_call() { let reply = "Рисую твою радужную пещеру! 🎨";