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                "content": segment
207                    .content
208                    .to_bridge_value()
209                    .expect("generated content bridge payload must serialize")
210            }),
211        }
212    }
213}
214
215/// A plain-text transcript segment.
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub struct TextSegment {
218    pub id: Option<String>,
219    pub text: String,
220}
221
222impl TextSegment {
223    /// Create a text segment.
224    #[must_use]
225    pub fn new(text: impl Into<String>) -> Self {
226        Self {
227            id: None,
228            text: text.into(),
229        }
230    }
231}
232
233/// A structured transcript segment.
234#[derive(Debug, Clone, PartialEq)]
235pub struct StructuredSegment {
236    pub id: Option<String>,
237    pub source: String,
238    pub content: GeneratedContent,
239}
240
241impl StructuredSegment {
242    /// Create a structured segment.
243    #[must_use]
244    pub fn new(source: impl Into<String>, content: GeneratedContent) -> Self {
245        Self {
246            id: None,
247            source: source.into(),
248            content,
249        }
250    }
251}
252
253/// A transcript response format.
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct ResponseFormat {
256    name: Option<String>,
257    schema: GenerationSchema,
258}
259
260impl ResponseFormat {
261    /// Create a response format from a generation schema.
262    #[must_use]
263    pub fn json_schema(schema: GenerationSchema) -> Self {
264        Self { name: None, schema }
265    }
266
267    /// Create a response format from a [`Generable`] Rust type.
268    ///
269    /// # Errors
270    ///
271    /// Returns an [`FMError`] if the type cannot produce a generation schema.
272    pub fn generating<T>() -> Result<Self, FMError>
273    where
274        T: Generable,
275    {
276        Ok(Self::json_schema(T::generation_schema()?))
277    }
278
279    pub(crate) fn from_transcript_json_value(value: &Value) -> Result<Self, FMError> {
280        let schema = value
281            .get("jsonSchema")
282            .and_then(|json_schema| json_schema.get("schema"))
283            .ok_or_else(|| {
284                FMError::DecodingFailure("response format is missing jsonSchema.schema".into())
285            })?;
286        let name = value
287            .get("jsonSchema")
288            .and_then(|json_schema| json_schema.get("name"))
289            .and_then(Value::as_str)
290            .map(ToOwned::to_owned);
291        Ok(Self {
292            name,
293            schema: GenerationSchema::from_json_schema_unchecked(
294                serde_json::to_string(schema).map_err(|error| {
295                    FMError::InvalidArgument(format!(
296                        "response format schema is not valid JSON: {error}"
297                    ))
298                })?,
299            ),
300        })
301    }
302
303    /// Attach an explicit display name.
304    #[must_use]
305    pub fn with_name(mut self, name: impl Into<String>) -> Self {
306        self.name = Some(name.into());
307        self
308    }
309
310    /// Display name FoundationModels associates with this response format.
311    #[must_use]
312    pub fn name(&self) -> String {
313        self.name
314            .clone()
315            .or_else(|| self.schema.name())
316            .unwrap_or_else(|| "GeneratedContent".to_string())
317    }
318
319    /// The underlying schema.
320    #[must_use]
321    pub const fn schema(&self) -> &GenerationSchema {
322        &self.schema
323    }
324
325    pub(crate) fn to_transcript_json_value(&self) -> Value {
326        let schema_value: Value = serde_json::from_str(self.schema.json_schema())
327            .expect("validated generation schema must always be valid JSON");
328        json!({
329            "type": "jsonSchema",
330            "jsonSchema": {
331                "name": self.name(),
332                "schema": schema_value,
333            }
334        })
335    }
336}
337
338/// A transcript tool definition.
339#[derive(Debug, Clone, PartialEq, Eq)]
340pub struct ToolDefinition {
341    pub name: String,
342    pub description: String,
343    pub parameters: GenerationSchema,
344}
345
346impl ToolDefinition {
347    /// Create a tool definition.
348    #[must_use]
349    pub fn new(
350        name: impl Into<String>,
351        description: impl Into<String>,
352        parameters: GenerationSchema,
353    ) -> Self {
354        Self {
355            name: name.into(),
356            description: description.into(),
357            parameters,
358        }
359    }
360
361    pub(crate) fn to_transcript_json_value(&self) -> Value {
362        let parameters: Value = serde_json::from_str(self.parameters.json_schema())
363            .expect("validated generation schema must always be valid JSON");
364        json!({
365            "type": "function",
366            "function": {
367                "name": self.name,
368                "description": self.description,
369                "parameters": parameters,
370            }
371        })
372    }
373}
374
375/// Convert a Rust value into a FoundationModels prompt.
376pub trait ToPrompt {
377    /// Convert the value into a prompt.
378    fn to_prompt(self) -> Result<Prompt, FMError>;
379}
380
381impl ToPrompt for Prompt {
382    fn to_prompt(self) -> Result<Prompt, FMError> {
383        Ok(self)
384    }
385}
386
387impl ToPrompt for &Prompt {
388    fn to_prompt(self) -> Result<Prompt, FMError> {
389        Ok(self.clone())
390    }
391}
392
393impl ToPrompt for String {
394    fn to_prompt(self) -> Result<Prompt, FMError> {
395        Ok(Prompt::from(self))
396    }
397}
398
399impl ToPrompt for &str {
400    fn to_prompt(self) -> Result<Prompt, FMError> {
401        Ok(Prompt::from(self))
402    }
403}
404
405impl ToPrompt for GeneratedContent {
406    fn to_prompt(self) -> Result<Prompt, FMError> {
407        Ok(Prompt::from(self))
408    }
409}
410
411impl ToPrompt for &GeneratedContent {
412    fn to_prompt(self) -> Result<Prompt, FMError> {
413        Ok(Prompt::from(self.clone()))
414    }
415}
416
417/// Convert a Rust value into FoundationModels instructions.
418pub trait ToInstructions {
419    /// Convert the value into instructions.
420    fn to_instructions(self) -> Result<Instructions, FMError>;
421}
422
423impl ToInstructions for Instructions {
424    fn to_instructions(self) -> Result<Instructions, FMError> {
425        Ok(self)
426    }
427}
428
429impl ToInstructions for &Instructions {
430    fn to_instructions(self) -> Result<Instructions, FMError> {
431        Ok(self.clone())
432    }
433}
434
435impl ToInstructions for String {
436    fn to_instructions(self) -> Result<Instructions, FMError> {
437        Ok(Instructions::from(self))
438    }
439}
440
441impl ToInstructions for &str {
442    fn to_instructions(self) -> Result<Instructions, FMError> {
443        Ok(Instructions::from(self))
444    }
445}
446
447impl ToInstructions for GeneratedContent {
448    fn to_instructions(self) -> Result<Instructions, FMError> {
449        Ok(Instructions::from(self))
450    }
451}
452
453impl ToInstructions for &GeneratedContent {
454    fn to_instructions(self) -> Result<Instructions, FMError> {
455        Ok(Instructions::from(self.clone()))
456    }
457}