use alloc::{borrow::Cow, boxed::Box, format, string::ToString, vec::Vec};
use core::{future::Future, pin::Pin};
use super::types::{PromptArgumentDefinition, PromptDefinition, PromptOutput, ToolError};
pub trait RustPrompt: Send + Sync {
type Params: serde::de::DeserializeOwned + schemars::JsonSchema + Send;
const NAME: &'static str;
const DESCRIPTION: &'static str;
fn description(&self) -> Cow<'static, str> {
Cow::Borrowed(Self::DESCRIPTION)
}
fn render(
&self,
params: Self::Params,
) -> impl Future<Output = Result<PromptOutput, ToolError>> + Send;
}
#[must_use]
pub fn definition_of_prompt<T: RustPrompt>(prompt: &T) -> PromptDefinition {
let schema = schemars::schema_for!(T::Params);
let val = match serde_json::to_value(&schema) {
Ok(v) => v,
Err(e) => {
tracing::error!(
prompt = T::NAME,
error = %e,
"failed to serialize prompt schema, producing empty definition"
);
return PromptDefinition {
name: T::NAME.to_string(),
description: prompt.description().into_owned(),
arguments: Vec::new(),
};
}
};
let mut arguments = Vec::new();
if let Some(obj) = val.as_object() {
let required_fields: Vec<&str> = obj
.get("required")
.and_then(|r| r.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
if let Some(props) = obj.get("properties").and_then(|p| p.as_object()) {
for (name, prop) in props {
let description = prop
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("")
.to_string();
let required = required_fields.contains(&name.as_str());
arguments.push(PromptArgumentDefinition {
name: name.clone(),
description,
required,
});
}
}
}
PromptDefinition {
name: T::NAME.to_string(),
description: prompt.description().into_owned(),
arguments,
}
}
pub type BoxPromptFuture<'a> =
Pin<Box<dyn Future<Output = Result<PromptOutput, ToolError>> + Send + 'a>>;
pub trait ErasedPrompt: Send + Sync {
fn definition(&self) -> PromptDefinition;
fn render_erased(&self, args: serde_json::Value) -> BoxPromptFuture<'_>;
}
impl<T: RustPrompt> ErasedPrompt for T {
fn definition(&self) -> PromptDefinition {
definition_of_prompt(self)
}
fn render_erased(&self, args: serde_json::Value) -> BoxPromptFuture<'_> {
Box::pin(async move {
let params: T::Params = serde_json::from_value(args).map_err(|e| {
ToolError::new(format!("Failed to deserialize prompt parameters: {e}"))
})?;
self.render(params).await
})
}
}