Skip to main content

foundation_models/
prompt.rs

1//! Prompt and instructions builders.
2
3use serde_json::{json, Value};
4
5use crate::content::GeneratedContent;
6use crate::error::FMError;
7use crate::schema::{Generable, GenerationSchema};
8
9/// A FoundationModels prompt.
10#[derive(Debug, Clone, PartialEq, Default)]
11pub struct Prompt {
12    segments: Vec<Segment>,
13}
14
15impl Prompt {
16    /// Create an empty prompt.
17    #[must_use]
18    pub const fn new() -> Self {
19        Self {
20            segments: Vec::new(),
21        }
22    }
23
24    /// Create a prompt from a single text segment.
25    #[must_use]
26    pub fn text(text: impl Into<String>) -> Self {
27        Self::from(text.into())
28    }
29
30    /// Create a prompt from a structured content segment.
31    #[must_use]
32    pub fn structured(content: GeneratedContent) -> Self {
33        Self::from(content)
34    }
35
36    /// Append a text segment.
37    pub fn push_text(&mut self, text: impl Into<String>) {
38        self.segments.push(Segment::text(text));
39    }
40
41    /// Append a structured content segment.
42    pub fn push_structured(&mut self, source: impl Into<String>, content: GeneratedContent) {
43        self.segments.push(Segment::structure(source, content));
44    }
45
46    /// Borrow the prompt segments.
47    #[must_use]
48    pub fn segments(&self) -> &[Segment] {
49        &self.segments
50    }
51
52    /// Consume the prompt and return its segments.
53    #[must_use]
54    pub fn into_segments(self) -> Vec<Segment> {
55        self.segments
56    }
57
58    pub(crate) fn to_bridge_value(&self) -> Value {
59        json!({
60            "segments": self.segments.iter().map(Segment::to_bridge_value).collect::<Vec<_>>()
61        })
62    }
63
64    pub(crate) fn to_bridge_json(&self) -> Result<String, FMError> {
65        serde_json::to_string(&self.to_bridge_value()).map_err(|error| {
66            FMError::InvalidArgument(format!("prompt is not JSON-serializable: {error}"))
67        })
68    }
69}
70
71impl From<String> for Prompt {
72    fn from(text: String) -> Self {
73        Self {
74            segments: vec![Segment::text(text)],
75        }
76    }
77}
78
79impl From<&str> for Prompt {
80    fn from(text: &str) -> Self {
81        Self::from(text.to_owned())
82    }
83}
84
85impl From<GeneratedContent> for Prompt {
86    fn from(content: GeneratedContent) -> Self {
87        Self {
88            segments: vec![Segment::structure("GeneratedContent", content)],
89        }
90    }
91}
92
93impl From<Vec<Segment>> for Prompt {
94    fn from(segments: Vec<Segment>) -> Self {
95        Self { segments }
96    }
97}
98
99/// A FoundationModels instructions value.
100#[derive(Debug, Clone, PartialEq, Default)]
101pub struct Instructions {
102    segments: Vec<Segment>,
103}
104
105impl Instructions {
106    /// Create empty instructions.
107    #[must_use]
108    pub const fn new() -> Self {
109        Self {
110            segments: Vec::new(),
111        }
112    }
113
114    /// Append a text segment.
115    pub fn push_text(&mut self, text: impl Into<String>) {
116        self.segments.push(Segment::text(text));
117    }
118
119    /// Append a structured content segment.
120    pub fn push_structured(&mut self, source: impl Into<String>, content: GeneratedContent) {
121        self.segments.push(Segment::structure(source, content));
122    }
123
124    /// Borrow the instruction segments.
125    #[must_use]
126    pub fn segments(&self) -> &[Segment] {
127        &self.segments
128    }
129
130    /// Consume the instructions and return their segments.
131    #[must_use]
132    pub fn into_segments(self) -> Vec<Segment> {
133        self.segments
134    }
135
136    pub(crate) fn to_bridge_value(&self) -> Value {
137        json!({
138            "segments": self.segments.iter().map(Segment::to_bridge_value).collect::<Vec<_>>()
139        })
140    }
141
142    pub(crate) fn to_bridge_json(&self) -> Result<String, FMError> {
143        serde_json::to_string(&self.to_bridge_value()).map_err(|error| {
144            FMError::InvalidArgument(format!("instructions are not JSON-serializable: {error}"))
145        })
146    }
147}
148
149impl From<String> for Instructions {
150    fn from(text: String) -> Self {
151        Self {
152            segments: vec![Segment::text(text)],
153        }
154    }
155}
156
157impl From<&str> for Instructions {
158    fn from(text: &str) -> Self {
159        Self::from(text.to_owned())
160    }
161}
162
163impl From<GeneratedContent> for Instructions {
164    fn from(content: GeneratedContent) -> Self {
165        Self {
166            segments: vec![Segment::structure("GeneratedContent", content)],
167        }
168    }
169}
170
171impl From<Vec<Segment>> for Instructions {
172    fn from(segments: Vec<Segment>) -> Self {
173        Self { segments }
174    }
175}
176
177/// A prompt or transcript segment.
178#[derive(Debug, Clone, PartialEq)]
179pub enum Segment {
180    Text(TextSegment),
181    Structure(StructuredSegment),
182}
183
184impl Segment {
185    /// Create a text segment.
186    #[must_use]
187    pub fn text(text: impl Into<String>) -> Self {
188        Self::Text(TextSegment::new(text))
189    }
190
191    /// Create a structured segment.
192    #[must_use]
193    pub fn structure(source: impl Into<String>, content: GeneratedContent) -> Self {
194        Self::Structure(StructuredSegment::new(source, content))
195    }
196
197    pub(crate) fn to_bridge_value(&self) -> Value {
198        match self {
199            Self::Text(segment) => json!({
200                "kind": "text",
201                "text": segment.text,
202            }),
203            Self::Structure(segment) => json!({
204                "kind": "structure",
205                "source": segment.source,
206                "contentJSON": segment.content.json_string().expect("generated content must serialize")
207            }),
208        }
209    }
210}
211
212/// A plain-text transcript segment.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct TextSegment {
215    pub id: Option<String>,
216    pub text: String,
217}
218
219impl TextSegment {
220    /// Create a text segment.
221    #[must_use]
222    pub fn new(text: impl Into<String>) -> Self {
223        Self {
224            id: None,
225            text: text.into(),
226        }
227    }
228}
229
230/// A structured transcript segment.
231#[derive(Debug, Clone, PartialEq)]
232pub struct StructuredSegment {
233    pub id: Option<String>,
234    pub source: String,
235    pub content: GeneratedContent,
236}
237
238impl StructuredSegment {
239    /// Create a structured segment.
240    #[must_use]
241    pub fn new(source: impl Into<String>, content: GeneratedContent) -> Self {
242        Self {
243            id: None,
244            source: source.into(),
245            content,
246        }
247    }
248}
249
250/// A transcript response format.
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct ResponseFormat {
253    name: Option<String>,
254    schema: GenerationSchema,
255}
256
257impl ResponseFormat {
258    /// Create a response format from a generation schema.
259    #[must_use]
260    pub fn json_schema(schema: GenerationSchema) -> Self {
261        Self { name: None, schema }
262    }
263
264    /// Create a response format from a [`Generable`] Rust type.
265    ///
266    /// # Errors
267    ///
268    /// Returns an [`FMError`] if the type cannot produce a generation schema.
269    pub fn generating<T>() -> Result<Self, FMError>
270    where
271        T: Generable,
272    {
273        Ok(Self::json_schema(T::generation_schema()?))
274    }
275
276    pub(crate) fn from_transcript_json_value(value: &Value) -> Result<Self, FMError> {
277        let schema = value
278            .get("jsonSchema")
279            .and_then(|json_schema| json_schema.get("schema"))
280            .ok_or_else(|| {
281                FMError::DecodingFailure("response format is missing jsonSchema.schema".into())
282            })?;
283        let name = value
284            .get("jsonSchema")
285            .and_then(|json_schema| json_schema.get("name"))
286            .and_then(Value::as_str)
287            .map(ToOwned::to_owned);
288        Ok(Self {
289            name,
290            schema: GenerationSchema::from_json_schema_unchecked(
291                serde_json::to_string(schema).map_err(|error| {
292                    FMError::InvalidArgument(format!(
293                        "response format schema is not valid JSON: {error}"
294                    ))
295                })?,
296            ),
297        })
298    }
299
300    /// Attach an explicit display name.
301    #[must_use]
302    pub fn with_name(mut self, name: impl Into<String>) -> Self {
303        self.name = Some(name.into());
304        self
305    }
306
307    /// Display name FoundationModels associates with this response format.
308    #[must_use]
309    pub fn name(&self) -> String {
310        self.name
311            .clone()
312            .or_else(|| self.schema.name())
313            .unwrap_or_else(|| "GeneratedContent".to_string())
314    }
315
316    /// The underlying schema.
317    #[must_use]
318    pub const fn schema(&self) -> &GenerationSchema {
319        &self.schema
320    }
321
322    pub(crate) fn to_transcript_json_value(&self) -> Value {
323        let schema_value: Value = serde_json::from_str(self.schema.json_schema())
324            .expect("validated generation schema must always be valid JSON");
325        json!({
326            "type": "jsonSchema",
327            "jsonSchema": {
328                "name": self.name(),
329                "schema": schema_value,
330            }
331        })
332    }
333}
334
335/// A transcript tool definition.
336#[derive(Debug, Clone, PartialEq, Eq)]
337pub struct ToolDefinition {
338    pub name: String,
339    pub description: String,
340    pub parameters: GenerationSchema,
341}
342
343impl ToolDefinition {
344    /// Create a tool definition.
345    #[must_use]
346    pub fn new(
347        name: impl Into<String>,
348        description: impl Into<String>,
349        parameters: GenerationSchema,
350    ) -> Self {
351        Self {
352            name: name.into(),
353            description: description.into(),
354            parameters,
355        }
356    }
357
358    pub(crate) fn to_transcript_json_value(&self) -> Value {
359        let parameters: Value = serde_json::from_str(self.parameters.json_schema())
360            .expect("validated generation schema must always be valid JSON");
361        json!({
362            "type": "function",
363            "function": {
364                "name": self.name,
365                "description": self.description,
366                "parameters": parameters,
367            }
368        })
369    }
370}
371
372/// Convert a Rust value into a FoundationModels prompt.
373pub trait ToPrompt {
374    /// Convert the value into a prompt.
375    fn to_prompt(self) -> Result<Prompt, FMError>;
376}
377
378impl ToPrompt for Prompt {
379    fn to_prompt(self) -> Result<Prompt, FMError> {
380        Ok(self)
381    }
382}
383
384impl ToPrompt for &Prompt {
385    fn to_prompt(self) -> Result<Prompt, FMError> {
386        Ok(self.clone())
387    }
388}
389
390impl ToPrompt for String {
391    fn to_prompt(self) -> Result<Prompt, FMError> {
392        Ok(Prompt::from(self))
393    }
394}
395
396impl ToPrompt for &str {
397    fn to_prompt(self) -> Result<Prompt, FMError> {
398        Ok(Prompt::from(self))
399    }
400}
401
402impl ToPrompt for GeneratedContent {
403    fn to_prompt(self) -> Result<Prompt, FMError> {
404        Ok(Prompt::from(self))
405    }
406}
407
408impl ToPrompt for &GeneratedContent {
409    fn to_prompt(self) -> Result<Prompt, FMError> {
410        Ok(Prompt::from(self.clone()))
411    }
412}
413
414/// Convert a Rust value into FoundationModels instructions.
415pub trait ToInstructions {
416    /// Convert the value into instructions.
417    fn to_instructions(self) -> Result<Instructions, FMError>;
418}
419
420impl ToInstructions for Instructions {
421    fn to_instructions(self) -> Result<Instructions, FMError> {
422        Ok(self)
423    }
424}
425
426impl ToInstructions for &Instructions {
427    fn to_instructions(self) -> Result<Instructions, FMError> {
428        Ok(self.clone())
429    }
430}
431
432impl ToInstructions for String {
433    fn to_instructions(self) -> Result<Instructions, FMError> {
434        Ok(Instructions::from(self))
435    }
436}
437
438impl ToInstructions for &str {
439    fn to_instructions(self) -> Result<Instructions, FMError> {
440        Ok(Instructions::from(self))
441    }
442}
443
444impl ToInstructions for GeneratedContent {
445    fn to_instructions(self) -> Result<Instructions, FMError> {
446        Ok(Instructions::from(self))
447    }
448}
449
450impl ToInstructions for &GeneratedContent {
451    fn to_instructions(self) -> Result<Instructions, FMError> {
452        Ok(Instructions::from(self.clone()))
453    }
454}