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