aimx/inference/prompt.rs
1//! Prompt generation for AI model inference
2//!
3//! This module generates structured prompts from AIMX workflow rules for AI inference. It transforms
4//! modifier rules (UPPERCASE identifiers) and assignment rules (`_` prefix) into well-formatted
5//! prompts that guide AI models to produce the desired outputs.
6//!
7//! # Overview
8//!
9//! When an AIMX expression contains an inference call (e.g., `$tool.function_name(args)`),
10//! the prompt generation system:
11//!
12//! 1. **Scans workflow rules** for relevant modifier and assignment rules
13//! 2. **Structures the prompt** into logical sections (ROLE, INSTRUCTIONS, OUTPUT FORMAT, etc.)
14//! 3. **Formats output rules** with examples and validation criteria
15//! 4. **Generates system and user prompts** optimized for the model's capability level
16//!
17//! # Core Concepts
18//!
19//! ## Modifier Rules
20//!
21//! Modifier rules are workflow rules with UPPERCASE identifiers that construct the prompt:
22//!
23//! - **`ROLE`**: Defines the AI's persona and primary function
24//! - **`INSTRUCTIONS`**: Provides task-specific guidance
25//! - **`OUTPUT`**: Specifies the desired output format and validation rules
26//! - **`SYSTEM`**: Overrides the default system prompt
27//! - **Other UPPERCASE rules**: General prompt modifiers and guidelines
28//!
29//! ## Assignment Rules
30//!
31//! Assignment rules (prefixed with `_`) are the targets that AI inference will populate:
32//!
33//! - `_variable_name`: Populated by AI with the parsed value
34//! - Rules with `Value::Format` provide formatting instructions and examples
35//!
36//! ## Capability-Based Formatting
37//!
38//! The prompt structure adapts to the model's capability level:
39//!
40//! - **`Capability::Minimal`**: Simple prompts with basic formatting
41//! - **`Capability::Limited`**: Moderate complexity with section headers
42//! - **`Capability::Standard`**: Full formatting with rich examples and instructions
43//!
44//! # Prompt Structure
45//!
46//! The generated prompt follows this standardized structure:
47//!
48//! ```text
49//! ROLE SECTION
50//! You are a helpful assistant.
51//!
52//! GUIDELINES SECTION
53//! - First guideline
54//! - Second guideline
55//!
56//! INSTRUCTIONS SECTION
57//! Derive the following values from the content:
58//! - variable_name: Description of what to extract
59//!
60//! OUTPUT FORMAT SECTION
61//! Provide your answer in this exact format:
62//! variable_name: <expected_format>
63//!
64//! EXAMPLES SECTION
65//! USER TEXT: "Example input text"
66//! variable_name: Example output value
67//!
68//! USER TEXT SECTION
69//! Actual input text to process
70//! ```
71//!
72//! # Error Handling
73//!
74//! The module returns [`anyhow::Result`] for error handling. Common errors include:
75//!
76//! - Missing assignment rules (nothing to populate)
77//! - Invalid rule configurations
78//! - Formatting errors during prompt generation
79//!
80//! # Integration with AIMX Workflows
81//!
82//! This module is typically used internally by the inference system when processing
83//! expressions like `$tool.function_name(args)`. The generated prompts are sent to
84//! AI providers, and the responses are used to populate assignment rules in workflows.
85
86use crate::{
87 inference::Capability,
88 WorkflowLike,
89 Rule,
90 Value,
91 Writer,
92 Prefix,
93};
94use std::{
95 collections::HashMap,
96 sync::Arc,
97};
98use anyhow::{Result, anyhow};
99
100/// Internal formatting helper for generating structured prompts
101///
102/// This struct handles the formatting details for different capability levels,
103/// ensuring that prompts are appropriately structured for the target AI model.
104/// It manages section headers, delimiters, and formatting styles.
105struct OutputFormat {
106 /// The writer used to generate the formatted prompt output
107 pub writer: Writer,
108 /// Prefix style for ordered lists (currently unused)
109 #[allow(dead_code)]
110 ordered: Prefix,
111 /// Prefix style for unordered lists
112 unordered: Prefix,
113 /// Prefix string for section headers
114 section_prefix: &'static str,
115 /// Suffix string for section headers
116 section_suffix: &'static str,
117 /// Delimiter between keys and values
118 delimiter: &'static str,
119 /// Delimiter for multiline content (currently unused)
120 #[allow(dead_code)]
121 multiline: &'static str,
122 /// Suffix for each line
123 suffix: &'static str,
124}
125
126impl OutputFormat {
127 /// Create a new OutputFormat configured for the given capability level
128 ///
129 /// Different capability levels use different formatting styles to match the
130 /// capabilities of various AI models:
131 ///
132 /// # Capability Levels
133 ///
134 /// - **`Minimal`**: Simple formatting with brackets and newlines, suitable for
135 /// basic models that struggle with complex formatting
136 /// - **`Limited`**: Moderate formatting with section headers and colons,
137 /// appropriate for models with moderate comprehension abilities
138 /// - **`Standard`**: Rich formatting with markdown-style headers and structured
139 /// delimiters, ideal for advanced models that can handle complex layouts
140 fn new(capability: &Capability) -> Self {
141 match capability {
142 Capability::Minimal => OutputFormat {
143 writer: Writer::formulizer(),
144
145 ordered: Prefix::None,
146 unordered: Prefix::None,
147 section_prefix: "[",
148 section_suffix: "]\n",
149 delimiter: "\n",
150 multiline: "\n",
151 suffix: "\n",
152 },
153 Capability::Limited => OutputFormat {
154 writer: Writer::formulizer(),
155
156 ordered: Prefix::Unordered,
157 unordered: Prefix::Unordered,
158 section_prefix: "===",
159 section_suffix: "===\n",
160 delimiter: ":\n",
161 multiline: ":\n",
162 suffix: "\n",
163 },
164 Capability::Standard => OutputFormat {
165 writer: Writer::formulizer(),
166
167 ordered: Prefix::Ordered,
168 unordered: Prefix::Unordered,
169 section_prefix: "## ",
170 section_suffix: "\n",
171 delimiter: ": ",
172 multiline: ":\n",
173 suffix: "\n",
174 },
175/*
176todo!() // Add xml and json maybe?
177*/
178 }
179 }
180
181 /// Start a new section with the given title
182 ///
183 /// This method writes the section prefix, the section title, and the section suffix
184 /// to the internal writer. The exact formatting depends on the capability level
185 /// configured for this OutputFormat instance.
186 ///
187 /// # Arguments
188 ///
189 /// * `string` - The title of the section to start
190 fn section_start(&mut self, string: &str) {
191 self.writer.write_str(self.section_prefix);
192 self.writer.write_str(string);
193 self.writer.write_str(self.section_suffix);
194 }
195
196 /// End the current section
197 ///
198 /// This method writes the section suffix to the internal writer, effectively
199 /// ending the current section. The exact formatting depends on the capability
200 /// level configured for this OutputFormat instance.
201 fn section_end(&mut self) {
202 self.writer.write_str(self.suffix);
203 }
204
205 /// Write a single item with key-value formatting
206 ///
207 /// This method writes a key-value pair with appropriate formatting based on
208 /// the configured capability level. For unordered lists, it adds a bullet point
209 /// prefix before the key.
210 ///
211 /// # Arguments
212 ///
213 /// * `key` - The key or label for the item
214 /// * `text` - The value or description for the item
215 fn single_item(&mut self, key: &str, text: &str) {
216 if self.unordered == Prefix::Unordered {
217 self.writer.write_str("- ");
218 }
219 self.writer.write_str(key);
220 self.writer.write_str(self.delimiter);
221 self.writer.write_str(text);
222 self.writer.write_str(self.suffix);
223 }
224
225 /// Write a single line with key-value formatting
226 ///
227 /// This method writes a key-value pair using the configured delimiter and suffix.
228 /// Unlike `single_item`, it does not add any list prefix regardless of the
229 /// unordered setting.
230 ///
231 /// # Arguments
232 ///
233 /// * `key` - The key or label for the line
234 /// * `text` - The value or content for the line
235 fn single_line(&mut self, key: &str, text: &str) {
236 self.writer.write_str(key);
237 self.writer.write_str(self.delimiter);
238 self.writer.write_str(text);
239 self.writer.write_str(self.suffix);
240 }
241
242 /// Write a single line with quoted text
243 ///
244 /// This method writes a key-value pair where the value is enclosed in quotes.
245 /// It automatically chooses between double quotes and single quotes based on
246 /// whether the text content contains double quotes.
247 ///
248 /// # Arguments
249 ///
250 /// * `key` - The key or label for the line
251 /// * `text` - The text content to be quoted
252 fn single_line_quoted(&mut self, key: &str, text: &str) {
253 self.writer.write_str(key);
254 self.writer.write_str(self.delimiter);
255 if text.contains('"') {
256 self.writer.write_char('\'');
257 self.writer.write_str(text);
258 self.writer.write_char('\'');
259 }
260 else {
261 self.writer.write_char('"');
262 self.writer.write_str(text);
263 self.writer.write_char('"');
264 }
265 self.writer.write_str(self.suffix);
266 }
267
268 /// Write a single line with tagged text (e.g., `<value>`)
269 ///
270 /// This method writes a key-value pair where the value is enclosed in angle brackets.
271 /// This is typically used for format specifications in output sections.
272 ///
273 /// # Arguments
274 ///
275 /// * `key` - The key or label for the line
276 /// * `text` - The text content to be enclosed in angle brackets
277 fn single_line_tagged(&mut self, key: &str, text: &str) {
278 self.writer.write_str(key);
279 self.writer.write_str(self.delimiter);
280 self.writer.write_char('<');
281 self.writer.write_str(text);
282 self.writer.write_char('>');
283 self.writer.write_str(self.suffix);
284 }
285
286 /// Write a value using the writer's print_value method
287 ///
288 /// This method writes a Value using the writer's print_value method with no prefix.
289 /// It's typically used for writing rule values directly.
290 ///
291 /// # Arguments
292 ///
293 /// * `value` - The Value to write
294 fn value(&mut self, value: &Value) {
295 self.writer.print_value(&Prefix::None, value);
296 self.writer.write_str("\n");
297 }
298
299 /// Write plain text
300 ///
301 /// This method writes plain text followed by a newline.
302 ///
303 /// # Arguments
304 ///
305 /// * `string` - The text to write
306 fn text(&mut self, string: &str) {
307 self.writer.write_str(string);
308 self.writer.write_str("\n");
309 }
310
311 #[allow(dead_code)]
312 /// Write a value with ordered prefix formatting
313 ///
314 /// This method writes a Value using the writer's print_value method with
315 /// ordered prefix formatting. Currently unused but available for future use.
316 ///
317 /// # Arguments
318 ///
319 /// * `value` - The Value to write
320 fn ordered_value(&mut self, value: &Value) {
321 self.writer.print_value(&self.ordered, value);
322 self.writer.write_str("\n");
323 }
324
325 /// Write a value with unordered prefix formatting
326 ///
327 /// This method writes a Value using the writer's print_value method with
328 /// unordered prefix formatting. It's typically used for writing modifier rules
329 /// in guideline sections.
330 ///
331 /// # Arguments
332 ///
333 /// * `value` - The Value to write
334 fn unordered_value(&mut self, value: &Value) {
335 self.writer.print_value(&self.unordered, value);
336 self.writer.write_str("\n");
337 }
338}
339
340/// Generate structured prompts from workflow rules for AI model inference
341///
342/// This function analyzes a workflow and converts its rules into a structured prompt
343/// suitable for AI model inference. It handles modifier rules (UPPERCASE identifiers)
344/// and assignment rules (`_` prefix) to create a well-formatted prompt that guides
345/// AI models to produce the desired outputs.
346///
347/// # Arguments
348///
349/// * `workflow` - An Arc-wrapped workflow containing the rules to process
350/// * `capability` - The capability level of the target AI model, which determines
351/// the formatting complexity of the generated prompt
352///
353/// # Returns
354///
355/// Returns a tuple containing:
356/// - `system_prompt`: The system-level prompt (can be overridden by SYSTEM rule)
357/// - `user_prompt`: The structured user prompt with sections and formatting
358///
359/// # Errors
360///
361/// Returns an error if:
362/// - No assignment rules are found (nothing to populate)
363/// - Workflow rules cannot be processed
364///
365pub fn generate_prompt(workflow: Arc<dyn WorkflowLike>, capability: &Capability) -> Result<(String, String)> {
366 let mut format = OutputFormat::new(capability);
367
368 // "ROLE" keyword modifier
369 let mut role_modifier: Option<Rule> = None;
370 // "INSTRUCTIONS" keyword modifier
371 let mut instructions_modifier: Option<Rule> = None;
372 // "OUTPUT_FORMAT" keyword modifier
373 let mut output_modifier: Option<Rule> = None;
374 // "SYSTEM" prompt (default)
375 let mut system_prompt: String = "You are a helpful assistant".to_owned();
376
377 // "content" keyword
378 let mut content: Option<Rule> = None;
379
380 // General prompt modifier_rules
381 let mut modifier_rules: Vec<Rule> = Vec::new();
382 // Output rules
383 let mut output_rules: HashMap<String, Rule> = HashMap::new();
384 // Output rule formatting and examples
385 let mut format_rules: Vec<Rule> = Vec::new();
386
387 // Iterate each rule
388 for rule in workflow.iter_rules() {
389 // Select inference output rules (rule identifiers with leading underscores)
390 if rule.is_assignment() {
391 // Create a map of ucid inference rules
392 let ucid = rule.ucid();
393 output_rules.insert(ucid, rule.clone());
394 }
395 }
396 if output_rules.len() == 0 {
397 return Err(anyhow!("Nothing To Assign"))
398 }
399 // Iterate each rule
400 for rule in workflow.iter_rules() {
401 let identifier = rule.identifier();
402 // content := user text
403 if identifier == "content" {
404 content = Some(rule.clone());
405 // Select modifier rules (rule identifiers that are all uppercase)
406 } else if rule.is_modifier() {
407 // Role persona
408 if identifier == "ROLE" {
409 role_modifier = Some(rule.clone());
410 // Agentic instructions
411 } else if identifier == "INSTRUCTIONS" {
412 instructions_modifier = Some(rule.clone());
413 // Output instructions and example user text
414 } else if identifier == "OUTPUT" {
415 output_modifier = Some(rule.clone());
416 // System prompt
417 } else if identifier == "SYSTEM" {
418 let value = rule.value();
419 system_prompt = value.to_pretty(Prefix::None);
420 // Specific rule format, specification and examples
421 } else if rule.is_format() && output_rules.contains_key(identifier) {
422 format_rules.push(rule.clone());
423 // General prompt modifiers
424 } else {
425 modifier_rules.push(rule.clone());
426 }
427 }
428 }
429
430 // ROLE keyword modifier
431 // You are a precise data extraction assistant. Your task is to extract specific weather information from user text.
432 //
433 if let Some(rule) = role_modifier {
434 format.value(rule.value());
435 } else {
436 format.text("You are a helpful assistant.");
437 }
438 format.section_end();
439
440 // General prompt modifier_rules
441 //
442 // ===GUIDELINES===
443 // - If a value is not present, use "Unknown".
444 // - Extract only the specific values requested.
445 // - Do not include additional commentary.
446 // - When multiple temperatures are mentioned, extract the first complete temperature value (number + optional unit)
447 // - A "temperature" is a numeric value that represents heat measurement, not other measurements like swell height, pressure, etc."
448
449 for rule in modifier_rules {
450 format.section_start(rule.identifier());
451 format.unordered_value(rule.value());
452 }
453 format.section_end();
454
455 // INSTRUCTIONS keyword modifier
456 //
457 // ===INSTRUCTIONS===
458 // Extract the following values from the user text:
459 // - TEMPERATURE: The numeric temperature value
460 // - UNIT: The temperature unit (Fahrenheit, Celsius, F, C, or "Unknown" if not specified)
461 //
462 format.section_start("INSTRUCTIONS");
463 if let Some(rule) = instructions_modifier {
464 format.value(rule.value());
465 } else {
466 format.text("Derive the following values from the content:");
467 }
468 for rule in &format_rules {
469 if let Value::Format(f) = rule.value() {
470 format.single_item(rule.identifier(), f.instruction());
471 }
472 }
473 format.section_end();
474
475 // OUTPUT_FORMAT keyword modifier
476 //
477 // ===OUTPUT FORMAT===
478 // Provide your answer in this exact format:
479 // TEMPERATURE: <number>
480 // UNIT: <Fahrenheit|Celsius|F|C|Unknown>
481 //
482 format.section_start("OUTPUT FORMAT");
483 if let Some(rule) = &output_modifier {
484 if let Value::Format(f) = rule.value() {
485 format.text(f.instruction());
486 } else {
487 format.value(rule.value());
488 }
489 } else {
490 format.text("Provide your answer in this exact format:");
491 }
492 for rule in &format_rules {
493 if let Value::Format(f) = rule.value() {
494 format.single_line_tagged(rule.identifier(), f.format());
495 }
496 }
497 format.section_end();
498
499 // ===EXAMPLES===
500 // USER TEXT: "It's 72 degrees Fahrenheit today"
501 // TEMPERATURE: 72
502 // UNIT: Fahrenheit
503 //
504 // USER TEXT: "I saw the temperature was 25C at noon"
505 // TEMPERATURE: 25
506 // UNIT: C
507 //
508 // USER TEXT: "It was really hot at 34 degrees"
509 // TEMPERATURE: 34
510 // UNIT: Unknown
511 //
512 // USER TEXT: "No weather information here"
513 // TEMPERATURE: Unknown
514 // UNIT: Unknown
515 //
516 format.section_start("EXAMPLES");
517 let mut index: usize = 0;
518 let mut length: usize = 1;
519 while index < length {
520 if let Some(rule) = &output_modifier {
521 if let Value::Format(f) = rule.value() {
522 length = f.array().len();
523 if index < length {
524 format.single_line_quoted("USER TEXT", &f.array()[index]);
525 }
526 }
527 }
528 for rule in &format_rules {
529 if let Value::Format(f) = rule.value() {
530 length = f.array().len();
531 if index < length {
532 format.single_line(rule.identifier(), &f.array()[index]);
533 }
534 }
535 }
536 format.section_end();
537 index += 1;
538 }
539
540 // ===USER TEXT===
541 if let Some(rule) = &content {
542 format.section_start("USER TEXT");
543 format.value(rule.value());
544 format.section_end();
545 }
546
547 Ok((system_prompt, format.writer.finish()))
548}