diff --git a/crates/openplotva-app/src/dialog_jobs/input.rs b/crates/openplotva-app/src/dialog_jobs/input.rs index 91283e83..92548adc 100644 --- a/crates/openplotva-app/src/dialog_jobs/input.rs +++ b/crates/openplotva-app/src/dialog_jobs/input.rs @@ -404,13 +404,7 @@ impl PostgresDialogInputMaterializer { scheduled.chat_id, scheduled.message_id ), })?; - let mut params = scheduled.clone(); - params.message_text = message.text; - params.original_text = message.original_text; - params.user_id = message.user_id; - params.user_full_name = message.name; - params.thread_id = (message.thread_id != 0).then_some(message.thread_id); - params.meta = serde_json::to_value(message.meta).unwrap_or_default(); + let params = canonical_dialog_job_params(scheduled, message); Ok((params, trigger_at, payload)) } @@ -914,6 +908,39 @@ impl PostgresDialogInputMaterializer { } } +pub(crate) fn canonical_dialog_job_params( + scheduled: &DialogJobParams, + mut message: HistoryMessage, +) -> DialogJobParams { + let scheduled_text = if scheduled.original_text.trim().is_empty() { + &scheduled.message_text + } else { + &scheduled.original_text + }; + let canonical_text = if message.original_text.trim().is_empty() { + &message.text + } else { + &message.original_text + }; + let requested_tool = scheduled.meta["requested_tool"] + .as_str() + .unwrap_or_default(); + if scheduled.user_id == message.user_id + && scheduled_text.trim() == canonical_text.trim() + && !requested_tool.is_empty() + { + message.meta.requested_tool = requested_tool.to_owned(); + } + let mut params = scheduled.clone(); + params.message_text = message.text; + params.original_text = message.original_text; + params.user_id = message.user_id; + params.user_full_name = message.name; + params.thread_id = (message.thread_id != 0).then_some(message.thread_id); + params.meta = serde_json::to_value(message.meta).unwrap_or_default(); + params +} + #[must_use] pub fn dialog_input_from_materialized_context( params: &DialogJobParams, @@ -1516,6 +1543,40 @@ mod context_artifact_tests { use super::*; use openplotva_memory::{Card, RetrievedMemory}; + #[test] + fn canonical_trigger_preserves_intent_only_for_the_unchanged_request() { + for (original, text, user_id, expected) in [ + ("", "draw a fish", 42, "draw_image"), + ("draw a fish", "draw a fish", 42, "draw_image"), + ("draw a fish", "never mind", 42, ""), + ("draw a fish", "draw a fish", 43, ""), + ] { + let scheduled = DialogJobParams { + chat_id: -100, + message_id: 7, + user_id: 42, + user_full_name: "User".into(), + message_text: "draw a fish".into(), + original_text: original.into(), + meta: serde_json::json!({"requested_tool":"draw_image"}), + max_output_tokens: 321, + thread_id: None, + }; + let canonical = canonical_dialog_job_params( + &scheduled, + HistoryMessage { + user_id, + text: text.into(), + ..Default::default() + }, + ); + let input = dialog_input_from_job_params_at(&canonical, OffsetDateTime::UNIX_EPOCH); + assert_eq!(input.message.meta.requested_tool, expected); + assert_eq!(input.message.text, text); + assert_eq!(input.user.id, user_id); + } + } + #[test] fn membership_gate_distinguishes_unknown_restricted_and_departed_users() { let member = |status: &str, is_member| openplotva_storage::ChatMemberRecord { diff --git a/crates/openplotva-app/src/dialog_jobs/tests.rs b/crates/openplotva-app/src/dialog_jobs/tests.rs index 8135c79a..8656ea4f 100644 --- a/crates/openplotva-app/src/dialog_jobs/tests.rs +++ b/crates/openplotva-app/src/dialog_jobs/tests.rs @@ -3115,6 +3115,7 @@ impl DialogToolCallHistoryStore for ToolHistoryStub { #[derive(Clone, Default)] struct MaterializerStub { history: Vec, + canonical_trigger: Option, observed: Arc>>, } @@ -3199,7 +3200,12 @@ impl DialogInputMaterializer for MaterializerStub { now: OffsetDateTime, ) -> DialogInputMaterializerFuture<'a> { Box::pin(async move { - let mut input = dialog_input_from_job_params_at(params, now); + let canonical = self + .canonical_trigger + .clone() + .map(|message| super::input::canonical_dialog_job_params(params, message)); + let mut input = + dialog_input_from_job_params_at(canonical.as_ref().unwrap_or(params), now); input.history = self.history.clone(); Ok(input) }) @@ -6352,6 +6358,15 @@ async fn explicit_image_request_repairs_text_into_a_native_draw() -> Result<(), let queue = InMemoryTaskQueue::new(); let mut params = dialog_params("Плотва, нарисуй рыбу в космосе"); params.meta = serde_json::json!({"requested_tool": "draw_image"}); + let materializer = MaterializerStub { + canonical_trigger: Some(HistoryMessage { + message_id: params.message_id, + user_id: params.user_id, + text: params.message_text.clone(), + ..Default::default() + }), + ..Default::default() + }; queue.assign(DIALOG_AIFARM_QUEUE_NAME, new_dialog_job_at(params, now)); let provider = StepProviderStub::with_steps(vec![ Ok(step_text("Сначала я подумаю, как изобразить эту рыбу.")), @@ -6384,7 +6399,7 @@ async fn explicit_image_request_repairs_text_into_a_native_draw() -> Result<(), &queue, &provider, &effects, - &BasicDialogInputMaterializer, + &materializer, &NoopDialogToolCallHistoryStore, session_options(now, &outcomes, &wiring), )