use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{ToolError, name::ToolName, schema::JsonSchema};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ToolInteractionCapabilities {
pub form: bool,
pub form_schema_validation: bool,
pub text: bool,
pub multiline_text: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ToolInteractionId(String);
impl ToolInteractionId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4().to_string())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for ToolInteractionId {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ToolInteractionRequest {
pub id: ToolInteractionId,
pub tool_name: ToolName,
pub call_id: Option<String>,
pub prompt: String,
pub kind: ToolInteractionKind,
}
impl ToolInteractionRequest {
pub(crate) fn validate_response(
&self,
response: &ToolInteractionResponse,
) -> Result<(), ToolError> {
if response.interaction_id() != &self.id {
return Err(ToolError::InvalidInteraction {
id: response.interaction_id().as_str().to_owned(),
message: format!(
"response id does not match pending interaction `{}`",
self.id.as_str()
),
});
}
match (response, &self.kind) {
(
ToolInteractionResponse::Submit { content, .. },
ToolInteractionKind::Form {
schema,
schema_validation: true,
},
) => {
if !content.is_object() {
return Err(ToolError::InvalidInteraction {
id: self.id.as_str().to_owned(),
message: "form responses must be JSON objects".to_owned(),
});
}
let issues = JsonSchema::validate_raw(schema, content)?;
if issues.is_empty() {
Ok(())
} else {
Err(ToolError::InvalidArguments {
tool: self.tool_name.clone(),
message: "form response failed schema validation".to_owned(),
issues,
})
}
}
(
ToolInteractionResponse::Submit { content, .. },
ToolInteractionKind::Form {
schema_validation: false,
..
},
) if content.is_object() => Ok(()),
(ToolInteractionResponse::Submit { content, .. }, ToolInteractionKind::Text { .. })
if content.is_string() =>
{
Ok(())
}
(ToolInteractionResponse::Submit { .. }, _) => Err(ToolError::InvalidInteraction {
id: self.id.as_str().to_owned(),
message: "interaction response content does not match request kind".to_owned(),
}),
(
ToolInteractionResponse::Decline { .. } | ToolInteractionResponse::Cancel { .. },
_,
) => Ok(()),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum ToolInteractionKind {
Form {
schema: serde_json::Value,
schema_validation: bool,
},
Text {
multiline: bool,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "decision", rename_all = "snake_case", deny_unknown_fields)]
pub enum ToolInteractionResponse {
Submit {
id: ToolInteractionId,
content: serde_json::Value,
},
Decline {
id: ToolInteractionId,
reason: Option<String>,
},
Cancel {
id: ToolInteractionId,
reason: Option<String>,
},
}
pub(crate) enum InteractionResolution {
Submit(serde_json::Value),
Decline(Option<String>),
Cancel(Option<String>),
}
impl ToolInteractionResponse {
#[must_use]
pub fn submit(id: ToolInteractionId, content: serde_json::Value) -> Self {
Self::Submit { id, content }
}
#[must_use]
pub fn decline(id: ToolInteractionId, reason: Option<String>) -> Self {
Self::Decline { id, reason }
}
#[must_use]
pub fn cancel(id: ToolInteractionId, reason: Option<String>) -> Self {
Self::Cancel { id, reason }
}
#[must_use]
pub fn interaction_id(&self) -> &ToolInteractionId {
match self {
Self::Submit { id, .. } | Self::Decline { id, .. } | Self::Cancel { id, .. } => id,
}
}
pub(crate) fn into_resolution(self) -> InteractionResolution {
match self {
Self::Submit { content, .. } => InteractionResolution::Submit(content),
Self::Decline { reason, .. } => InteractionResolution::Decline(reason),
Self::Cancel { reason, .. } => InteractionResolution::Cancel(reason),
}
}
}