Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 29 additions & 35 deletions crates/openplotva-app/src/dialog_jobs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4472,30 +4472,27 @@ async fn session_sends_second_uncited_web_citation_repair_when_exhausted()
}

#[tokio::test]
async fn captured_session_reaction_discards_draft_and_returns_final_once()
async fn captured_session_delivers_the_reply_beside_a_lone_reaction_once()
-> Result<(), Box<dyn Error>> {
let now = OffsetDateTime::from_unix_timestamp(1_779_193_800)?;
let provider = StepProviderStub::with_steps(vec![
Ok(step_tools(
"Смешно вышло.",
vec![(
"reaction-call",
openplotva_dialog::ToolStep {
step: openplotva_dialog::STEP_REACT_TO_MESSAGE.to_owned(),
emoji: "🤣".to_owned(),
target_message_id: 100,
..openplotva_dialog::ToolStep::default()
},
)],
)),
Ok(step_text("Смешно вышло.")),
]);
let provider = StepProviderStub::with_steps(vec![Ok(step_tools(
"Смешно вышло.",
vec![(
"reaction-call",
openplotva_dialog::ToolStep {
step: openplotva_dialog::STEP_REACT_TO_MESSAGE.to_owned(),
emoji: "🤣".to_owned(),
target_message_id: 100,
..openplotva_dialog::ToolStep::default()
},
)],
))]);
let toolbox = SessionToolboxStub::default();
let input = dialog_input_from_job_params_at(&dialog_params("рассмеши"), now);

let output = crate::dialog_turn::run_captured_session(&provider, &toolbox, input, 8).await?;

assert_eq!(provider.calls(), 2);
assert_eq!(provider.calls(), 1);
assert_eq!(output.messages, vec!["Смешно вышло."]);
assert_eq!(output.tool_calls.len(), 1);
Ok(())
Expand Down Expand Up @@ -6149,30 +6146,26 @@ async fn session_react_to_message_validates_emoji_and_repeats() -> Result<(), Bo
}

#[tokio::test]
async fn session_reaction_discards_draft_and_delivers_followup_final() -> Result<(), Box<dyn Error>>
{
async fn session_delivers_the_reply_beside_a_lone_reaction() -> Result<(), Box<dyn Error>> {
let now = OffsetDateTime::from_unix_timestamp(1_779_193_800)?;
let queue = InMemoryTaskQueue::new();
let job_id = queue.assign(
DIALOG_AIFARM_QUEUE_NAME,
new_dialog_job_at(dialog_params("покажи юмор"), now),
);
let reply = "Ну и юмор, будто из трясины выудили. Почти как наши шутки в чате.";
let provider = StepProviderStub::with_steps(vec![
Ok(step_tools(
reply,
vec![(
"reaction-call",
openplotva_dialog::ToolStep {
step: openplotva_dialog::STEP_REACT_TO_MESSAGE.to_owned(),
emoji: "🤣".to_owned(),
target_message_id: 100,
..openplotva_dialog::ToolStep::default()
},
)],
)),
Ok(step_text(reply)),
]);
let provider = StepProviderStub::with_steps(vec![Ok(step_tools(
reply,
vec![(
"reaction-call",
openplotva_dialog::ToolStep {
step: openplotva_dialog::STEP_REACT_TO_MESSAGE.to_owned(),
emoji: "🤣".to_owned(),
target_message_id: 100,
..openplotva_dialog::ToolStep::default()
},
)],
))]);
let reactor = Arc::new(ReactorStub::default());
let wiring = session_wiring(
Arc::new(SessionToolboxStub::default()),
Expand All @@ -6194,8 +6187,9 @@ async fn session_reaction_discards_draft_and_delivers_followup_final() -> Result
)
.await;

assert_eq!(provider.calls(), 2);
assert_eq!(provider.calls(), 1, "the reply needs no second model call");
assert!(effects.intermediates().is_empty());
assert_eq!(effects.sent().len(), 1);
assert_eq!(effects.sent()[0].1, reply);
assert_eq!(
reactor.reactions.lock().expect("reactions").clone(),
Expand Down
32 changes: 24 additions & 8 deletions crates/openplotva-app/src/dialog_turn/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,10 @@ struct SessionState {
search_citation_repairs: i32,
#[serde(default, rename = "repeated_failed_tool_call")]
repeated_tool_call: bool,
/// Reply the model wrote beside a lone reaction; the next round delivers it as
/// the final answer instead of asking the model again.
#[serde(default)]
carried_reply: Option<String>,
}

#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
Expand Down Expand Up @@ -552,6 +556,7 @@ where
links,
search_citation_repairs,
repeated_tool_call,
carried_reply: None,
});
// A crash after a committed intent has an unknown outcome. Never repeat the effect.
for event in ctx
Expand Down Expand Up @@ -606,8 +611,8 @@ where
if let Err(error) = persist_agent_event(queue, effects, ctx.item_id, AGENT_CHECKPOINT_EVENT_STAGE, BTreeMap::from([("checkpoint".into(), serde_json::to_string(&(base_input, &state)).expect("serializable agent state"))]), ctx.now).await {
return std::ops::ControlFlow::Break(agent_persistence_failed(error));
}
let SessionState { mut requested_tool_call_offset, mut tool_context, mut gift_used, mut budget, mut agent, mut sent, mut side_effect_tickets, mut recorded_tool_calls, mut regenerations, mut anti_loop, mut repeated_final_repair, mut requires_novel_final, mut draws_scheduled, mut songs_scheduled, mut reacted_message_ids, mut tool_result_cache, mut media_reference_aliases, mut successful_web_search, mut web_source_urls, mut links, mut search_citation_repairs, mut repeated_tool_call } = state;
macro_rules! next_step { () => { std::ops::ControlFlow::Continue((SessionState { requested_tool_call_offset, tool_context, gift_used, budget, agent, sent, side_effect_tickets, recorded_tool_calls, regenerations, anti_loop, repeated_final_repair, requires_novel_final, draws_scheduled, songs_scheduled, reacted_message_ids, tool_result_cache, media_reference_aliases, successful_web_search, web_source_urls, links, search_citation_repairs, repeated_tool_call }, report)) }; }
let SessionState { mut requested_tool_call_offset, mut tool_context, mut gift_used, mut budget, mut agent, mut sent, mut side_effect_tickets, mut recorded_tool_calls, mut regenerations, mut anti_loop, mut repeated_final_repair, mut requires_novel_final, mut draws_scheduled, mut songs_scheduled, mut reacted_message_ids, mut tool_result_cache, mut media_reference_aliases, mut successful_web_search, mut web_source_urls, mut links, mut search_citation_repairs, mut repeated_tool_call, mut carried_reply } = state;
macro_rules! next_step { () => { std::ops::ControlFlow::Continue((SessionState { requested_tool_call_offset, tool_context, gift_used, budget, agent, sent, side_effect_tickets, recorded_tool_calls, regenerations, anti_loop, repeated_final_repair, requires_novel_final, draws_scheduled, songs_scheduled, reacted_message_ids, tool_result_cache, media_reference_aliases, successful_web_search, web_source_urls, links, search_citation_repairs, repeated_tool_call, carried_reply }, report)) }; }

let previously_observed_message_id = agent.observed_message_id;
if let Ok(Ok(messages)) = tokio::time::timeout(std::time::Duration::from_secs(2), materializer.observe_dialog_messages(active_params, agent.observed_message_id)).await {
Expand Down Expand Up @@ -673,11 +678,12 @@ where
max_iterations,
);
let iteration = agent.iteration;
if next == openplotva_agent::NextStep::Exhausted {
let carried = carried_reply.take();
if next == openplotva_agent::NextStep::Exhausted && carried.is_none() {
return std::ops::ControlFlow::Break( session_exhausted(ctx.item_id, &sent, &side_effect_tickets, &budget, round_now));
}

let force_final = next == openplotva_agent::NextStep::Final;
let force_final = carried.is_none() && next == openplotva_agent::NextStep::Final;
let tools = if base_input.disable_tools {
ToolsMode::Disabled
} else if force_final
Expand Down Expand Up @@ -706,7 +712,10 @@ where
let provider_deadline = Instant::now()
+ std::time::Duration::try_from(budget.remaining(round_now)).unwrap_or_default();
let step_started = tokio::time::Instant::now();
let result = tokio::time::timeout(std::time::Duration::try_from(budget.remaining(round_now)).unwrap_or_default(), TURN_DEADLINE
let result = if let Some(text) = carried {
let (provider, model) = agent.preferred_target.clone().unwrap_or_default();
Ok(openplotva_dialog::ChatStepOutput { provider, model, text, tool_calls: Vec::new(), trace: None })
} else { tokio::time::timeout(std::time::Duration::try_from(budget.remaining(round_now)).unwrap_or_default(), TURN_DEADLINE
Comment thread
iamwavecut marked this conversation as resolved.
.scope(
Some(provider_deadline),
step_provider.run_chat_step(ChatStepRequest {
Expand All @@ -717,7 +726,8 @@ where
tools,
iteration: usize::try_from(iteration).unwrap_or(1),
}),
)).await.unwrap_or_else(|_| Err(Box::new(std::io::Error::new(std::io::ErrorKind::TimedOut, "Agent turn deadline reached"))));
)).await.unwrap_or_else(|_| Err(Box::new(std::io::Error::new(std::io::ErrorKind::TimedOut, "Agent turn deadline reached"))))
};
let failure_now =
ctx.now + TimeDuration::try_from(processing_started.elapsed()).unwrap_or_default();
let step = match result {
Expand Down Expand Up @@ -1481,8 +1491,14 @@ where
side_effect_tickets.extend(batch_side_effects);

match disposition {
SessionBatchDisposition::ContinueForResults
| SessionBatchDisposition::ContinueWithoutFinal => {}
SessionBatchDisposition::ContinueForResults => {}
SessionBatchDisposition::ContinueWithoutFinal => {
if step.tool_calls.iter().all(|call| call.step.step == STEP_REACT_TO_MESSAGE)
&& !links.prepare_response(&step.text).trim().is_empty()
{
carried_reply = Some(step.text.clone());
}
}
SessionBatchDisposition::CompleteWithSideEffect => {
let delivered_replies = step.tool_calls.iter().zip(&batch_results)
.filter(|(call, result)| tool_delivered_its_own_reply(&call.step.step, result))
Expand Down
9 changes: 6 additions & 3 deletions crates/openplotva-dialog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,8 @@ const ALTERNATIVE_DIALOG_TOOL_CATALOG: &[ToolSpec] = &[
summary: "Create or edit an image from the user's request.",
when_to_use: "Use when the latest user message asks to draw, generate, create, redraw, or edit an image.",
result: "Schedules image generation and ENDS your turn; the image arrives asynchronously. \
Anything you want to tell the chat, say it before or together with this call.",
Call it without accompanying text or a promise to draw: text alongside a tool \
call is not published.",
continuation: ToolContinuation::MayTerminateOnSuccess,
args: DRAW_IMAGE_ARGS,
},
Expand All @@ -812,7 +813,8 @@ const ALTERNATIVE_DIALOG_TOOL_CATALOG: &[ToolSpec] = &[
tales, prose, poems, roleplay, or other text-only creative writing unless \
the user explicitly asks to turn that writing into a song.",
result: "Schedules music generation and ENDS your turn; the song arrives asynchronously. \
Anything you want to tell the chat, say it before or together with this call.",
Call it without accompanying text or a promise to make the song: text alongside a \
tool call is not published.",
continuation: ToolContinuation::MayTerminateOnSuccess,
args: GENERATE_SONG_ARGS,
},
Expand Down Expand Up @@ -1146,7 +1148,8 @@ pub const SESSION_REACT_TO_MESSAGE_SPEC: ToolSpec = ToolSpec {
🖕 😈 😴 😭 🤓 👻 👨‍💻 🎃 🙈 😇 😨 🤝 🤗 🫡 🎅 🎄 ☃ 💅 🤪 🗿 🆒 💘 🙉 🦄 😘 💊 \
🙊 😎 👾 🤷‍♂ 🤷 🤷‍♀ 😡",
result: "Sets the reaction; returns ok, or an error for a disallowed emoji or a message you \
already reacted to this turn.",
already reacted to this turn. Text you return together with this call is delivered \
as your reply.",
continuation: ToolContinuation::Sidecar,
args: &[
ToolArgSpec {
Expand Down
Loading
Loading