1use alloc::{borrow::Cow, boxed::Box, format, string::ToString, vec::Vec};
4use core::{future::Future, pin::Pin};
5
6use super::types::{PromptArgumentDefinition, PromptDefinition, PromptOutput, ToolError};
7use crate::compat::{HashMap, HashMapIter};
8
9pub trait RustPrompt: Send + Sync {
11 type Params: serde::de::DeserializeOwned + schemars::JsonSchema + Send;
13
14 const NAME: &'static str;
16
17 const DESCRIPTION: &'static str;
19
20 fn description(&self) -> Cow<'static, str> {
22 Cow::Borrowed(Self::DESCRIPTION)
23 }
24
25 fn render(
27 &self,
28 params: Self::Params,
29 ) -> impl Future<Output = Result<PromptOutput, ToolError>> + Send;
30}
31
32pub fn definition_of_prompt<T: RustPrompt>(prompt: &T) -> Result<PromptDefinition, ToolError> {
38 let schema = schemars::schema_for!(T::Params);
39 let val = serde_json::to_value(&schema).map_err(|e| {
40 ToolError::new(format!(
41 "Failed to serialize schema for prompt '{}': {e}",
42 T::NAME
43 ))
44 })?;
45 let mut arguments = Vec::new();
46
47 if let Some(obj) = val.as_object() {
48 let required_fields: Vec<&str> = match obj.get("required").and_then(|r| r.as_array()) {
49 Some(arr) => arr.iter().filter_map(|v| v.as_str()).collect(),
50 None => Vec::new(),
52 };
53 if let Some(props) = obj.get("properties").and_then(|p| p.as_object()) {
54 for (name, prop) in props {
55 let description = prop
56 .get("description")
57 .and_then(|d| d.as_str())
58 .unwrap_or("")
59 .to_string();
60 let required = required_fields.contains(&name.as_str());
61 arguments.push(PromptArgumentDefinition {
62 name: name.clone(),
63 description,
64 required,
65 });
66 }
67 }
68 }
69
70 Ok(PromptDefinition {
71 name: T::NAME.to_string(),
72 description: prompt.description().into_owned(),
73 arguments,
74 })
75}
76
77pub(crate) type BoxPromptFuture<'a> =
79 Pin<Box<dyn Future<Output = Result<PromptOutput, ToolError>> + Send + 'a>>;
80
81pub(crate) trait ErasedPrompt: Send + Sync {
86 fn render_erased(&self, args: serde_json::Value) -> BoxPromptFuture<'_>;
88}
89
90impl<T: RustPrompt> ErasedPrompt for T {
91 fn render_erased(&self, args: serde_json::Value) -> BoxPromptFuture<'_> {
92 Box::pin(async move {
93 let params: T::Params = serde_json::from_value(args).map_err(|e| {
94 ToolError::new(format!("Failed to deserialize prompt parameters: {e}"))
95 })?;
96 self.render(params).await
97 })
98 }
99}
100
101struct RegisteredPrompt {
103 definition: PromptDefinition,
104 erased: Box<dyn ErasedPrompt>,
105}
106
107#[derive(Default)]
114pub struct PromptRegistry {
115 prompts: HashMap<&'static str, RegisteredPrompt>,
116}
117
118impl core::fmt::Debug for PromptRegistry {
119 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
120 let names: Vec<&str> = self.prompts.keys().copied().collect();
121 f.debug_struct("PromptRegistry")
122 .field("prompt_count", &self.prompts.len())
123 .field("prompt_names", &names)
124 .finish()
125 }
126}
127
128impl PromptRegistry {
129 #[must_use]
131 pub fn new() -> Self {
132 Self {
133 prompts: HashMap::new(),
134 }
135 }
136
137 pub fn register<P: RustPrompt + 'static>(&mut self, prompt: P) -> &mut Self {
147 if let Err(e) = self.try_register(prompt) {
148 panic!("Failed to build definition for prompt '{}': {e}", P::NAME);
149 }
150 self
151 }
152
153 pub fn try_register<P: RustPrompt + 'static>(
164 &mut self,
165 prompt: P,
166 ) -> Result<&mut Self, ToolError> {
167 let definition = definition_of_prompt(&prompt)?;
168 self.prompts.insert(
169 P::NAME,
170 RegisteredPrompt {
171 definition,
172 erased: Box::new(prompt),
173 },
174 );
175 Ok(self)
176 }
177
178 #[must_use]
185 pub fn with_prompt<P: RustPrompt + 'static>(mut self, prompt: P) -> Self {
186 self.register(prompt);
187 self
188 }
189
190 #[must_use]
194 pub fn definitions(&self) -> Vec<PromptDefinition> {
195 self.prompts
196 .values()
197 .map(|entry| entry.definition.clone())
198 .collect()
199 }
200
201 #[must_use]
203 pub fn len(&self) -> usize {
204 self.prompts.len()
205 }
206
207 #[must_use]
209 pub fn is_empty(&self) -> bool {
210 self.prompts.is_empty()
211 }
212
213 #[must_use]
215 pub fn contains(&self, name: &str) -> bool {
216 self.prompts.contains_key(name)
217 }
218
219 #[must_use]
224 pub fn definition(&self, name: &str) -> Option<&PromptDefinition> {
225 self.prompts.get(name).map(|entry| &entry.definition)
226 }
227
228 #[must_use]
232 pub fn iter(&self) -> PromptDefinitions<'_> {
233 PromptDefinitions {
234 inner: self.prompts.iter(),
235 }
236 }
237
238 pub async fn render(
248 &self,
249 name: &str,
250 args: serde_json::Value,
251 ) -> Option<Result<PromptOutput, ToolError>> {
252 let entry = self.prompts.get(name)?;
253 Some(entry.erased.render_erased(args).await)
254 }
255}
256
257pub struct PromptDefinitions<'a> {
262 inner: HashMapIter<'a, &'static str, RegisteredPrompt>,
263}
264
265impl Iterator for PromptDefinitions<'_> {
266 type Item = (&'static str, PromptDefinition);
267
268 fn next(&mut self) -> Option<Self::Item> {
269 self.inner
270 .next()
271 .map(|(name, entry)| (*name, entry.definition.clone()))
272 }
273
274 fn size_hint(&self) -> (usize, Option<usize>) {
275 self.inner.size_hint()
276 }
277}
278
279impl ExactSizeIterator for PromptDefinitions<'_> {
280 fn len(&self) -> usize {
281 self.inner.len()
282 }
283}
284
285impl<'a> IntoIterator for &'a PromptRegistry {
287 type Item = (&'static str, PromptDefinition);
288 type IntoIter = PromptDefinitions<'a>;
289
290 fn into_iter(self) -> Self::IntoIter {
291 self.iter()
292 }
293}