1use alloc::{borrow::Cow, boxed::Box, format, string::ToString, vec::Vec};
4use core::{future::Future, pin::Pin};
5
6use super::types::{
7 PromptArgumentDefinition, PromptDefinition, PromptOutput, RegistryItem, ToolError,
8};
9use crate::compat::{HashMap, HashMapIter};
10
11pub trait RustPrompt: Send + Sync {
13 type Params: serde::de::DeserializeOwned + schemars::JsonSchema + Send;
15
16 const NAME: &'static str;
18
19 const DESCRIPTION: &'static str;
21
22 fn description(&self) -> Cow<'static, str> {
24 Cow::Borrowed(Self::DESCRIPTION)
25 }
26
27 fn render(
29 &self,
30 params: Self::Params,
31 ) -> impl Future<Output = Result<PromptOutput, ToolError>> + Send;
32}
33
34#[derive(serde::Deserialize)]
36struct PromptPropertySchema {
37 description: Option<alloc::string::String>,
39}
40
41#[derive(serde::Deserialize)]
43struct PromptParamsSchema {
44 #[serde(rename = "type")]
46 schema_type: Option<alloc::string::String>,
47 required: Option<Vec<alloc::string::String>>,
49 properties: Option<alloc::collections::BTreeMap<alloc::string::String, PromptPropertySchema>>,
51}
52
53pub fn definition_of_prompt<T: RustPrompt>(prompt: &T) -> Result<PromptDefinition, ToolError> {
60 let schema = schemars::schema_for!(T::Params);
61 let val = serde_json::to_value(&schema).map_err(|e| {
62 ToolError::new(format!(
63 "Failed to serialize schema for prompt '{}': {e}",
64 T::NAME
65 ))
66 })?;
67 let parsed: PromptParamsSchema = serde_json::from_value(val).map_err(|e| {
68 ToolError::new(format!(
69 "Failed to parse object schema for prompt '{}': {e}",
70 T::NAME
71 ))
72 })?;
73 if parsed.schema_type.as_deref() != Some("object") {
74 return Err(ToolError::new(format!(
75 "Prompt '{}' Params schema must have type \"object\", got {:?}",
76 T::NAME,
77 parsed.schema_type
78 )));
79 }
80
81 let required_fields = parsed.required.as_deref().unwrap_or(&[]);
82 let arguments = parsed
83 .properties
84 .into_iter()
85 .flatten()
86 .map(|(name, prop)| {
87 let required = required_fields.iter().any(|req| req == &name);
88 PromptArgumentDefinition {
89 name,
90 description: prop.description.unwrap_or_else(alloc::string::String::new),
91 required,
92 }
93 })
94 .collect();
95
96 Ok(PromptDefinition {
97 name: T::NAME.to_string(),
98 description: prompt.description().into_owned(),
99 arguments,
100 })
101}
102
103pub type BoxPromptFuture<'a> =
105 Pin<Box<dyn Future<Output = Result<PromptOutput, ToolError>> + Send + 'a>>;
106
107pub trait ErasedPrompt: Send + Sync {
112 fn render_erased(&self, args: serde_json::Value) -> BoxPromptFuture<'_>;
114}
115
116impl<T: RustPrompt> ErasedPrompt for T {
117 fn render_erased(&self, args: serde_json::Value) -> BoxPromptFuture<'_> {
118 Box::pin(async move {
119 let params: T::Params = serde_json::from_value(args).map_err(|e| {
120 ToolError::new(format!("Failed to deserialize prompt parameters: {e}"))
121 })?;
122 self.render(params).await
123 })
124 }
125}
126
127struct RegisteredPrompt {
129 definition: PromptDefinition,
130 erased: Box<dyn ErasedPrompt>,
131}
132
133#[derive(Default)]
140pub struct PromptRegistry {
141 prompts: HashMap<&'static str, RegisteredPrompt>,
142}
143
144impl core::fmt::Debug for PromptRegistry {
145 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
146 let names: Vec<&str> = self.prompts.keys().copied().collect();
147 f.debug_struct("PromptRegistry")
148 .field("prompt_count", &self.prompts.len())
149 .field("prompt_names", &names)
150 .finish()
151 }
152}
153
154impl PromptRegistry {
155 #[must_use]
157 pub fn new() -> Self {
158 Self {
159 prompts: HashMap::new(),
160 }
161 }
162
163 pub fn register<P: RustPrompt + 'static>(&mut self, prompt: P) -> &mut Self {
173 if let Err(e) = self.try_register(prompt) {
174 panic!("Failed to build definition for prompt '{}': {e}", P::NAME);
175 }
176 self
177 }
178
179 pub fn try_register<P: RustPrompt + 'static>(
190 &mut self,
191 prompt: P,
192 ) -> Result<&mut Self, ToolError> {
193 let definition = definition_of_prompt(&prompt)?;
194 self.prompts.insert(
195 P::NAME,
196 RegisteredPrompt {
197 definition,
198 erased: Box::new(prompt),
199 },
200 );
201 Ok(self)
202 }
203
204 #[must_use]
211 pub fn with_prompt<P: RustPrompt + 'static>(mut self, prompt: P) -> Self {
212 self.register(prompt);
213 self
214 }
215
216 #[must_use]
220 pub fn definitions(&self) -> Vec<PromptDefinition> {
221 self.prompts
222 .values()
223 .map(|entry| entry.definition.clone())
224 .collect()
225 }
226
227 pub fn remove(&mut self, name: &str) -> bool {
229 self.prompts.remove(name).is_some()
230 }
231
232 pub fn clear(&mut self) {
234 self.prompts.clear();
235 }
236
237 #[must_use]
239 pub fn len(&self) -> usize {
240 self.prompts.len()
241 }
242
243 #[must_use]
245 pub fn is_empty(&self) -> bool {
246 self.prompts.is_empty()
247 }
248
249 #[must_use]
251 pub fn contains(&self, name: &str) -> bool {
252 self.prompts.contains_key(name)
253 }
254
255 #[must_use]
260 pub fn definition(&self, name: &str) -> Option<&PromptDefinition> {
261 self.prompts.get(name).map(|entry| &entry.definition)
262 }
263
264 #[must_use]
268 pub fn iter(&self) -> PromptDefinitions<'_> {
269 PromptDefinitions {
270 inner: self.prompts.iter(),
271 }
272 }
273
274 pub async fn render(
282 &self,
283 name: &str,
284 args: serde_json::Value,
285 ) -> Result<PromptOutput, ToolError> {
286 let Some(entry) = self.prompts.get(name) else {
287 return Err(ToolError::not_found(RegistryItem::Prompt, name));
288 };
289 entry.erased.render_erased(args).await
290 }
291}
292
293pub struct PromptDefinitions<'a> {
298 inner: HashMapIter<'a, &'static str, RegisteredPrompt>,
299}
300
301impl Iterator for PromptDefinitions<'_> {
302 type Item = (&'static str, PromptDefinition);
303
304 fn next(&mut self) -> Option<Self::Item> {
305 self.inner
306 .next()
307 .map(|(name, entry)| (*name, entry.definition.clone()))
308 }
309
310 fn size_hint(&self) -> (usize, Option<usize>) {
311 self.inner.size_hint()
312 }
313}
314
315impl ExactSizeIterator for PromptDefinitions<'_> {
316 fn len(&self) -> usize {
317 self.inner.len()
318 }
319}
320
321impl<'a> IntoIterator for &'a PromptRegistry {
323 type Item = (&'static str, PromptDefinition);
324 type IntoIter = PromptDefinitions<'a>;
325
326 fn into_iter(self) -> Self::IntoIter {
327 self.iter()
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 struct NonObjectPrompt;
336
337 impl RustPrompt for NonObjectPrompt {
338 type Params = alloc::string::String;
339 const NAME: &'static str = "non_object_prompt";
340 const DESCRIPTION: &'static str = "Prompt with non-object params";
341
342 async fn render(&self, params: Self::Params) -> Result<PromptOutput, ToolError> {
343 Ok(PromptOutput::user(params))
344 }
345 }
346
347 #[test]
348 fn definition_of_prompt_rejects_non_object_schema() {
349 let err = definition_of_prompt(&NonObjectPrompt)
350 .expect_err("non-object prompt schema must fail loudly");
351 assert!(
352 err.message.contains("must have type \"object\""),
353 "unexpected error: {}",
354 err.message
355 );
356 }
357}