Skip to main content

vespe/execute2/
content.rs

1//! This module defines the data structures used to represent content exchanged with
2//! external models (LLMs). It provides a structured way to build prompts, distinguishing
3//! between system instructions, user input, and agent responses. The core components
4//! are `ModelContentItem`, representing a single part of a conversation, and
5//! `ModelContent`, which aggregates multiple `ModelContentItem`s into a complete prompt.
6
7use serde::{Deserialize, Serialize};
8
9/// Represents content originating from the system.
10///
11/// This struct encapsulates text that provides instructions or context from the
12/// system to the model. It's typically used for setting up the model's behavior
13/// or providing high-level guidance.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SystemModelContent {
16    text: String,
17}
18
19/// Represents content originating from the user's prompt or context files.
20///
21/// This struct holds text input provided directly by the user or gathered from
22/// user-defined context files. It forms the primary input for the model's responses.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct UserModelContent {
25    text: String,
26}
27
28/// Represents content generated by an agent or a previous model turn.
29///
30/// This struct stores text that has been generated by an AI agent or a prior
31/// interaction with an external model. It allows for multi-turn conversations
32/// and contextual responses.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct AgentModelContent {
35    author: Option<String>,
36    text: String,
37}
38
39/// An enum that represents a single piece of content in a conversation or prompt
40/// sent to a model. It distinguishes between system instructions, user input,
41/// and agent responses.
42///
43/// Each variant wraps a specific content type ([`SystemModelContent`], [`UserModelContent`], [`AgentModelContent`]),
44/// allowing the execution engine to categorize and format different parts of a prompt
45/// appropriately for an external model.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub enum ModelContentItem {
48    /// Content provided by the system, typically for instructions or context.
49    System(SystemModelContent),
50    /// Content provided by the user, representing direct input or queries.
51    User(UserModelContent),
52    /// Content generated by an AI agent or a previous model response.
53    Agent(AgentModelContent),
54    /// Content to merge to next message as a prefix.
55    /// This is used internally to handle special merging logic during prompt construction.
56    MergeDownstream(String),
57    /// Content to merge to previous message as a postfix.
58    /// This is used internally to handle special merging logic during prompt construction.
59    MergeUpstream(String),
60}
61
62impl ModelContentItem {
63    /// Creates a new `ModelContentItem::User` from a string slice.
64    ///
65    /// # Arguments
66    ///
67    /// * `text` - The text content for the user message.
68    ///
69    /// # Returns
70    ///
71    /// A `ModelContentItem` variant containing the user's text.
72    pub fn user(text: &str) -> Self {
73        ModelContentItem::User(UserModelContent { text: text.into() })
74    }
75
76    /// Creates a new `ModelContentItem::System` from a string slice.
77    ///
78    /// # Arguments
79    ///
80    /// * `text` - The text content for the system message.
81    ///
82    /// # Returns
83    ///
84    /// A `ModelContentItem` variant containing the system's text.
85    pub fn system(text: &str) -> Self {
86        ModelContentItem::System(SystemModelContent { text: text.into() })
87    }
88
89    /// Creates a new `ModelContentItem::Agent` from a string slice.
90    ///
91    /// # Arguments
92    ///
93    /// * `author` - The author of the agent message.
94    /// * `text` - The text content for the agent's message.
95    ///
96    /// # Returns
97    ///
98    /// A `ModelContentItem` variant containing the agent's text.
99    pub fn agent(author: Option<String>, text: &str) -> Self {
100        ModelContentItem::Agent(AgentModelContent {
101            author,
102            text: text.into(),
103        })
104    }
105
106    /// Creates a new `ModelContentItem::MergeDownstream` from a string slice.
107    ///
108    /// This variant is used internally to indicate that its content should be prepended
109    /// to the next non-merge `ModelContentItem` during prompt construction.
110    ///
111    /// # Arguments
112    ///
113    /// * `text` - The text content to be merged downstream.
114    ///
115    /// # Returns
116    ///
117    /// A `ModelContentItem::MergeDownstream` variant.
118    pub fn merge_downstream(text: &str) -> Self {
119        ModelContentItem::MergeDownstream(text.into())
120    }
121
122    /// Creates a new `ModelContentItem::MergeUpstream` from a string slice.
123    ///
124    /// This variant is used internally to indicate that its content should be appended
125    /// to the previous non-merge `ModelContentItem` during prompt construction.
126    ///
127    /// # Arguments
128    ///
129    /// * `text` - The text content to be merged upstream.
130    ///
131    /// # Returns
132    ///
133    /// A `ModelContentItem::MergeUpstream` variant.
134    pub fn merge_upstream(text: &str) -> Self {
135        ModelContentItem::MergeUpstream(text.into())
136    }
137
138    /// Checks if the content of the `ModelContentItem` is empty or contains only whitespace.
139    ///
140    /// This is useful for filtering out empty messages during prompt construction.
141    ///
142    /// # Returns
143    ///
144    /// `true` if the item's text content is empty or only whitespace, `false` otherwise.
145    pub fn is_empty(&self) -> bool {
146        self.to_string().trim().is_empty()
147    }
148}
149
150impl ToString for ModelContentItem {
151    /// Converts the `ModelContentItem` into its raw text content.
152    ///
153    /// This implementation of `ToString` simply returns the underlying text
154    /// of the content item, without any additional formatting or headers.
155    ///
156    /// # Returns
157    ///
158    /// A `String` containing the raw text content.
159    fn to_string(&self) -> String {
160        match self {
161            ModelContentItem::System(content) => format!("{}", content.text),
162            ModelContentItem::User(content) => format!("{}", content.text),
163            ModelContentItem::Agent(content) => format!("{}", content.text),
164            ModelContentItem::MergeDownstream(content) => content.clone(),
165            ModelContentItem::MergeUpstream(content) => content.clone(),
166        }
167    }
168}
169
170/// Defines the various formats in which a prompt can be structured for an external model.
171#[derive(Clone, Copy)]
172pub enum PromptFormat {
173    /// Represents a prompt format where each `ModelContentItem` is treated as a distinct "part"
174    /// with specific headers (e.g., "System:", "User:", "Assistant:").
175    Parts,
176}
177
178/// Configuration for how the `ModelContent` should be formatted into a final prompt string.
179pub struct PromptConfig {
180    /// Optional: The name of the agent to be used in the prompt. If `with_agent_names` is true,
181    /// this name will be used to generate a unique agent identifier.
182    pub agent: Option<String>,
183    /// The desired format for the prompt (e.g., `PromptFormat::Parts`).
184    pub format: PromptFormat,
185    /// A boolean indicating whether agent names should be included in the formatted prompt.
186    pub with_agent_names: bool,
187    /// A boolean indicating whether an invitation for the assistant to respond should be appended
188    /// to the prompt (e.g., "Assistant:").
189    pub with_invitation: bool,
190}
191
192/// A struct representing a full conversation or a multi-part prompt.
193///
194/// `ModelContent` is a collection of `ModelContentItem`s, ordered to form a complete
195/// prompt that can be sent to an external model. It allows for building complex
196/// conversational contexts.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct ModelContent(pub Vec<ModelContentItem>);
199
200impl ModelContent {
201    /// Creates a new, empty `ModelContent` instance.
202    ///
203    /// # Returns
204    ///
205    /// A new `ModelContent` with an empty list of items.
206    pub fn new() -> Self {
207        ModelContent(Vec::new())
208    }
209
210    /// Creates a new `ModelContent` instance containing a single `ModelContentItem`.
211    ///
212    /// # Arguments
213    ///
214    /// * `item` - The `ModelContentItem` to be wrapped in a new `ModelContent`.
215    ///
216    /// # Returns
217    ///
218    /// A new `ModelContent` containing the provided item.
219    pub fn from_item(item: ModelContentItem) -> Self {
220        ModelContent(vec![item])
221    }
222
223    /// Extends the current `ModelContent` with items from another `ModelContent` instance.
224    ///
225    /// The items from the `content` parameter are appended to the end of the
226    /// current `ModelContent`'s list of items.
227    ///
228    /// # Arguments
229    ///
230    /// * `content` - Another `ModelContent` instance whose items will be appended.
231    pub fn extend(&mut self, content: ModelContent) {
232        self.0.extend(content.0.into_iter());
233    }
234
235    /// Appends a single `ModelContentItem` to the end of the `ModelContent`.
236    ///
237    /// # Arguments
238    ///
239    /// * `item` - The `ModelContentItem` to be added.
240    pub fn push(&mut self, item: ModelContentItem) {
241        self.0.push(item);
242    }
243
244    /// Formats a single `ModelContentItem` into a prompt string using the `PromptFormat::Parts` style.
245    ///
246    /// This internal helper method applies specific formatting rules for System, User, and Agent
247    /// content, including adding headers and handling agent names based on the provided `PromptConfig`.
248    ///
249    /// # Arguments
250    ///
251    /// * `item` - The `ModelContentItem` to format.
252    /// * `config` - The `PromptConfig` specifying formatting options.
253    ///
254    /// # Returns
255    ///
256    /// A `String` representing the formatted content item.
257    ///
258    /// # Panics
259    ///
260    /// This method will panic if it encounters a `MergeDownstream` or `MergeUpstream` item,
261    /// as these should have been handled in a prior merging pass.
262    fn embed_in_prompt_as_part(item: &ModelContentItem, config: &PromptConfig) -> String {
263        match item {
264            ModelContentItem::System(content) => {
265                let text = content.text.trim();
266                if !text.is_empty() {
267                    format!("---\nSystem:\n{}\n", text)
268                } else {
269                    text.into()
270                }
271            }
272            ModelContentItem::User(content) => {
273                let text = content.text.trim();
274                if !text.is_empty() {
275                    format!("---\nUser:\n{}\n", text)
276                } else {
277                    text.into()
278                }
279            }
280            ModelContentItem::Agent(content) => {
281                let text = content.text.trim();
282                if !text.is_empty() {
283                    let name = match config.with_agent_names {
284                        false => None,
285                        true => content
286                            .author
287                            .clone()
288                            .map(|x| super::names::generate_name(&x)),
289                    };
290                    if let Some(name) = name {
291                        format!("Assistant {}:\n{}\n", name, text)
292                    } else {
293                        format!("Assistant:\n{}\n", text)
294                    }
295                } else {
296                    text.into()
297                }
298            }
299            _ => {
300                panic!("cannot be embedded, bug!");
301            }
302        }
303    }
304
305    /// Dispatches to the appropriate embedding function based on the `PromptFormat` specified in the config.
306    ///
307    /// This method acts as a router for different prompt formatting strategies.
308    ///
309    /// # Arguments
310    ///
311    /// * `item` - The `ModelContentItem` to embed.
312    /// * `config` - The `PromptConfig` specifying the desired format.
313    ///
314    /// # Returns
315    ///
316    /// A `String` representing the embedded content item.
317    fn embed_in_prompt(item: &ModelContentItem, config: &PromptConfig) -> String {
318        match config.format {
319            PromptFormat::Parts => Self::embed_in_prompt_as_part(item, config),
320        }
321    }
322
323    /// Converts the entire `ModelContent` into a single formatted prompt string.
324    ///
325    /// This method iterates through all `ModelContentItem`s and formats each one
326    /// using `ModelContentItem::to_prompt()`, then joins them with newline characters.
327    /// The result is a complete prompt ready for an external model.
328    ///
329    /// # Returns
330    ///
331    /// A `String` representing the concatenated and formatted prompt.
332    pub fn to_prompt(&self, config: &PromptConfig) -> String {
333        let agent_name = match config.with_agent_names {
334            false => None,
335            true => config
336                .agent
337                .clone()
338                .map(|x| super::names::generate_name(&x)),
339        };
340
341        let identity = agent_name
342            .clone()
343            .map(|x| format!("You are {}.\n", x))
344            .unwrap_or(String::new());
345
346        // Pre pass: prepend identity message
347        let mut prepend_identity = vec![ModelContentItem::merge_downstream(&identity)];
348        prepend_identity.extend(self.0.clone());
349
350        // Pre pass: delete empty messages
351        let non_empty_items = &prepend_identity
352            .iter()
353            .filter(|x| !x.is_empty())
354            .collect::<Vec<&ModelContentItem>>();
355
356        // First pass: merge downstream and upstream messages
357        let mut merged_items: Vec<ModelContentItem> = Vec::new();
358        let mut downstream_merges: Vec<String> = Vec::new();
359
360        for item in non_empty_items {
361            match item {
362                ModelContentItem::System(_) | ModelContentItem::User(_) => {
363                    let mut current_text = item.to_string();
364
365                    if !downstream_merges.is_empty() {
366                        let prefix = downstream_merges.join("\n");
367                        current_text = format!("{}\n{}", prefix, current_text);
368                        downstream_merges.clear();
369                    }
370
371                    let new_item = match item {
372                        ModelContentItem::System(_) => ModelContentItem::system(&current_text),
373                        ModelContentItem::User(_) => ModelContentItem::user(&current_text),
374                        _ => unreachable!(), // We are in the match arm for these types
375                    };
376                    merged_items.push(new_item);
377                }
378                ModelContentItem::Agent(agent_item) => {
379                    let mut current_text = item.to_string();
380
381                    if !downstream_merges.is_empty() {
382                        let prefix = downstream_merges.join("\n");
383                        current_text = format!("{}\n{}", prefix, current_text);
384                        downstream_merges.clear();
385                    }
386
387                    let new_item =
388                        ModelContentItem::agent(agent_item.author.clone(), &current_text);
389
390                    merged_items.push(new_item);
391                }
392                ModelContentItem::MergeDownstream(s) => {
393                    downstream_merges.push(s.clone());
394                }
395                ModelContentItem::MergeUpstream(s) => {
396                    if let Some(last_item) = merged_items.last_mut() {
397                        let text_to_append = s.clone();
398                        match last_item {
399                            ModelContentItem::System(c) => {
400                                c.text = format!("{}\n{}", c.text, text_to_append)
401                            }
402                            ModelContentItem::User(c) => {
403                                c.text = format!("{}\n{}", c.text, text_to_append)
404                            }
405                            ModelContentItem::Agent(c) => {
406                                c.text = format!("{}\n{}", c.text, text_to_append)
407                            }
408                            _ => {} // Should not happen as we only push normal items
409                        }
410                    }
411                    // If there's no previous item, we ignore the upstream merge.
412                }
413            }
414        }
415
416        // Second pass: merge consecutive messages of the same type
417        let mut final_merged_items: Vec<ModelContentItem> = Vec::new();
418        let mut iter = merged_items.into_iter();
419
420        if let Some(mut last) = iter.next() {
421            for item in iter {
422                match (&mut last, &item) {
423                    (
424                        ModelContentItem::System(last_content),
425                        ModelContentItem::System(item_content),
426                    ) => {
427                        last_content.text.push('\n');
428                        last_content.text.push_str(&item_content.text);
429                    }
430                    (
431                        ModelContentItem::User(last_content),
432                        ModelContentItem::User(item_content),
433                    ) => {
434                        last_content.text.push('\n');
435                        last_content.text.push_str(&item_content.text);
436                    }
437                    (
438                        ModelContentItem::Agent(last_content),
439                        ModelContentItem::Agent(item_content),
440                    ) => {
441                        last_content.text.push('\n');
442                        last_content.text.push_str(&item_content.text);
443                    }
444                    _ => {
445                        final_merged_items.push(last);
446                        last = item;
447                    }
448                }
449            }
450            final_merged_items.push(last);
451        }
452
453        let mut prompt = final_merged_items
454            .iter()
455            .map(|item| Self::embed_in_prompt(item, config))
456            .collect::<Vec<String>>()
457            .join("\n");
458
459        let invitation = match (config.with_invitation, agent_name) {
460            (true, Some(agent_name)) => format!("Assistant {}:", agent_name),
461            (true, None) => format!("Assistant:"),
462            (false, _) => String::new(),
463        };
464
465        prompt.push_str(&invitation);
466
467        /*
468        Aggiungere questo con gemini e' peggiorativo per multi-persona (example 01), vedendo
469        di nuovo "Assistant" prende di nuovo la personalita' del pagliaccio e non del corvo.
470        prompt.push_str("Assistant:\n"); // TODO rendere dipendente dal tipo di formattazione?
471        */
472
473        prompt
474    }
475}
476
477impl Default for ModelContent {
478    /// Returns a default, empty `ModelContent` instance.
479    ///
480    /// This is equivalent to calling `ModelContent::new()`.
481    fn default() -> Self {
482        Self::new()
483    }
484}
485
486impl ToString for ModelContent {
487    /// Converts the entire `ModelContent` into a single raw text string.
488    ///
489    /// This method concatenates the raw text content of all `ModelContentItem`s,
490    /// without any additional formatting or message type headers.
491    ///
492    /// # Returns
493    ///
494    /// A `String` containing the concatenated raw text content.
495    fn to_string(&self) -> String {
496        self.0
497            .iter()
498            .map(|item| item.to_string())
499            .collect::<Vec<String>>()
500            .join("\n")
501    }
502}