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(¤t_text),
373 ModelContentItem::User(_) => ModelContentItem::user(¤t_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(), ¤t_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}