use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::core::event::Event;
use crate::genai_types::FunctionCall;
pub const REQUEST_CONFIRMATION_FUNCTION_NAME: &str = "adk_request_confirmation";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolConfirmation {
#[serde(default)]
pub hint: String,
#[serde(default)]
pub confirmed: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payload: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfirmationRequest {
pub original_function_call: FunctionCall,
pub tool_confirmation: ToolConfirmation,
}
#[derive(Debug, Default)]
pub struct ConfirmationOutcome {
pub responses: IndexMap<String, ToolConfirmation>,
}
#[derive(Debug, Default)]
pub struct ConfirmationPreprocessor;
impl ConfirmationPreprocessor {
#[must_use]
pub fn new() -> Self {
Self
}
pub fn process_event(&self, event: &Event) -> ConfirmationOutcome {
let mut out = ConfirmationOutcome::default();
if event.author != "user" {
return out;
}
let Some(content) = event.response.content.as_ref() else {
return out;
};
for part in &content.parts {
let crate::genai_types::Part::FunctionResponse(fr) = part else {
continue;
};
if fr.name != REQUEST_CONFIRMATION_FUNCTION_NAME {
continue;
}
let Some(id) = fr.id.clone().filter(|s| !s.is_empty()) else {
tracing::warn!(
"confirmation preprocessor: dropping adk_request_confirmation response \
with no function_call_id"
);
continue;
};
let payload = fr
.response
.get("toolConfirmation")
.or_else(|| fr.response.get("response"))
.unwrap_or(&fr.response);
match serde_json::from_value::<ToolConfirmation>(payload.clone()) {
Ok(c) => {
out.responses.insert(id, c);
}
Err(e) => {
tracing::warn!(
"confirmation preprocessor: malformed ToolConfirmation response: {e}"
);
}
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::llm_response::LlmResponse;
use crate::genai_types::{Content, FunctionResponse, Part, Role};
use serde_json::json;
fn user_event(id: &str, response: Value) -> Event {
Event::new(
"user",
LlmResponse {
content: Some(Content {
role: Role::User,
parts: vec![Part::FunctionResponse(FunctionResponse {
id: Some(id.into()),
name: REQUEST_CONFIRMATION_FUNCTION_NAME.into(),
response,
will_continue: None,
scheduling: None,
})],
}),
..LlmResponse::default()
},
)
}
#[test]
fn absorbs_bare_tool_confirmation() {
let ev = user_event("fc-1", json!({"confirmed": true, "hint": "ok?"}));
let out = ConfirmationPreprocessor::new().process_event(&ev);
assert!(out.responses.get("fc-1").unwrap().confirmed);
}
#[test]
fn absorbs_wrapped_tool_confirmation() {
let ev = user_event("fc-2", json!({"toolConfirmation": {"confirmed": false}}));
let out = ConfirmationPreprocessor::new().process_event(&ev);
assert!(!out.responses.get("fc-2").unwrap().confirmed);
}
#[test]
fn ignores_non_user_events() {
let mut ev = user_event("fc-3", json!({"confirmed": true}));
ev.author = "agent".into();
let out = ConfirmationPreprocessor::new().process_event(&ev);
assert!(out.responses.is_empty());
}
#[test]
fn confirmation_request_serializes_camel_case() {
let req = ConfirmationRequest {
original_function_call: FunctionCall::new("transfer_money", json!({"amount": 5})),
tool_confirmation: ToolConfirmation {
hint: "Approve transfer?".into(),
confirmed: false,
payload: None,
},
};
let v = serde_json::to_value(&req).unwrap();
assert!(v.get("originalFunctionCall").is_some());
assert_eq!(v["toolConfirmation"]["confirmed"], json!(false));
}
}