oxify_connect_llm/
prompt_engineering.rs

1//! Prompt Engineering Utilities
2//!
3//! This module provides utilities for constructing effective prompts using proven
4//! prompt engineering techniques like few-shot learning, chain-of-thought reasoning,
5//! role-based prompting, and more.
6//!
7//! # Examples
8//!
9//! ```
10//! use oxify_connect_llm::{FewShotPrompt, Example, ChainOfThought};
11//!
12//! // Few-shot learning
13//! let mut few_shot = FewShotPrompt::new("Classify the sentiment of the following text:");
14//! few_shot.add_example("I love this product!", "positive");
15//! few_shot.add_example("This is terrible.", "negative");
16//! few_shot.set_query("This is amazing!");
17//!
18//! let prompt = few_shot.build();
19//! assert!(prompt.contains("I love this product!"));
20//!
21//! // Chain-of-thought prompting
22//! let cot = ChainOfThought::new("What is 15% of 80?")
23//!     .with_instruction("Let's think step by step:");
24//! let prompt = cot.build();
25//! ```
26
27use std::fmt;
28
29/// Example for few-shot learning
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Example {
32    /// Input for the example
33    pub input: String,
34    /// Expected output for the example
35    pub output: String,
36}
37
38impl Example {
39    /// Create a new example
40    pub fn new(input: impl Into<String>, output: impl Into<String>) -> Self {
41        Self {
42            input: input.into(),
43            output: output.into(),
44        }
45    }
46}
47
48/// Few-shot prompt builder
49///
50/// Constructs prompts using few-shot learning by providing examples
51/// before the actual query.
52#[derive(Debug, Clone)]
53pub struct FewShotPrompt {
54    instruction: String,
55    examples: Vec<Example>,
56    query: Option<String>,
57    input_prefix: String,
58    output_prefix: String,
59    separator: String,
60}
61
62impl FewShotPrompt {
63    /// Create a new few-shot prompt with an instruction
64    pub fn new(instruction: impl Into<String>) -> Self {
65        Self {
66            instruction: instruction.into(),
67            examples: Vec::new(),
68            query: None,
69            input_prefix: "Input: ".to_string(),
70            output_prefix: "Output: ".to_string(),
71            separator: "\n\n".to_string(),
72        }
73    }
74
75    /// Add an example
76    pub fn add_example(
77        &mut self,
78        input: impl Into<String>,
79        output: impl Into<String>,
80    ) -> &mut Self {
81        self.examples.push(Example::new(input, output));
82        self
83    }
84
85    /// Add multiple examples
86    pub fn add_examples(&mut self, examples: Vec<Example>) -> &mut Self {
87        self.examples.extend(examples);
88        self
89    }
90
91    /// Set the query (the actual input to process)
92    pub fn set_query(&mut self, query: impl Into<String>) -> &mut Self {
93        self.query = Some(query.into());
94        self
95    }
96
97    /// Set custom input/output prefixes
98    pub fn with_prefixes(
99        &mut self,
100        input_prefix: impl Into<String>,
101        output_prefix: impl Into<String>,
102    ) -> &mut Self {
103        self.input_prefix = input_prefix.into();
104        self.output_prefix = output_prefix.into();
105        self
106    }
107
108    /// Set custom separator between examples
109    pub fn with_separator(&mut self, separator: impl Into<String>) -> &mut Self {
110        self.separator = separator.into();
111        self
112    }
113
114    /// Build the final prompt
115    pub fn build(&self) -> String {
116        let mut prompt = self.instruction.clone();
117        prompt.push_str(&self.separator);
118
119        // Add examples
120        for example in &self.examples {
121            prompt.push_str(&self.input_prefix);
122            prompt.push_str(&example.input);
123            prompt.push('\n');
124            prompt.push_str(&self.output_prefix);
125            prompt.push_str(&example.output);
126            prompt.push_str(&self.separator);
127        }
128
129        // Add query
130        if let Some(query) = &self.query {
131            prompt.push_str(&self.input_prefix);
132            prompt.push_str(query);
133            prompt.push('\n');
134            prompt.push_str(&self.output_prefix);
135        }
136
137        prompt
138    }
139}
140
141/// Chain-of-thought prompt builder
142///
143/// Encourages step-by-step reasoning by adding instructions
144/// that guide the model to think through the problem.
145#[derive(Debug, Clone)]
146pub struct ChainOfThought {
147    question: String,
148    instruction: String,
149    examples: Vec<(String, String)>, // (question, reasoning)
150}
151
152impl ChainOfThought {
153    /// Create a new chain-of-thought prompt
154    pub fn new(question: impl Into<String>) -> Self {
155        Self {
156            question: question.into(),
157            instruction: "Let's approach this step-by-step:".to_string(),
158            examples: Vec::new(),
159        }
160    }
161
162    /// Set custom instruction
163    pub fn with_instruction(mut self, instruction: impl Into<String>) -> Self {
164        self.instruction = instruction.into();
165        self
166    }
167
168    /// Add a reasoning example (question + step-by-step solution)
169    pub fn add_example(
170        mut self,
171        question: impl Into<String>,
172        reasoning: impl Into<String>,
173    ) -> Self {
174        self.examples.push((question.into(), reasoning.into()));
175        self
176    }
177
178    /// Build the final prompt
179    pub fn build(&self) -> String {
180        let mut prompt = String::new();
181
182        // Add examples first
183        for (q, reasoning) in &self.examples {
184            prompt.push_str("Question: ");
185            prompt.push_str(q);
186            prompt.push_str("\n\n");
187            prompt.push_str(reasoning);
188            prompt.push_str("\n\n---\n\n");
189        }
190
191        // Add the actual question
192        prompt.push_str("Question: ");
193        prompt.push_str(&self.question);
194        prompt.push_str("\n\n");
195        prompt.push_str(&self.instruction);
196
197        prompt
198    }
199}
200
201/// Role-based prompt builder
202///
203/// Constructs prompts with explicit role assignments (system, user, assistant)
204/// for better context and behavior control.
205#[derive(Debug, Clone)]
206pub struct RolePrompt {
207    system_message: Option<String>,
208    messages: Vec<(Role, String)>,
209}
210
211/// Role in a conversation
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum Role {
214    /// System message (sets behavior and context)
215    System,
216    /// User message
217    User,
218    /// Assistant message
219    Assistant,
220}
221
222impl fmt::Display for Role {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        match self {
225            Role::System => write!(f, "System"),
226            Role::User => write!(f, "User"),
227            Role::Assistant => write!(f, "Assistant"),
228        }
229    }
230}
231
232impl RolePrompt {
233    /// Create a new role-based prompt
234    pub fn new() -> Self {
235        Self {
236            system_message: None,
237            messages: Vec::new(),
238        }
239    }
240
241    /// Set system message
242    pub fn system(mut self, message: impl Into<String>) -> Self {
243        self.system_message = Some(message.into());
244        self
245    }
246
247    /// Add user message
248    pub fn user(mut self, message: impl Into<String>) -> Self {
249        self.messages.push((Role::User, message.into()));
250        self
251    }
252
253    /// Add assistant message
254    pub fn assistant(mut self, message: impl Into<String>) -> Self {
255        self.messages.push((Role::Assistant, message.into()));
256        self
257    }
258
259    /// Add a message with explicit role
260    pub fn add_message(mut self, role: Role, message: impl Into<String>) -> Self {
261        self.messages.push((role, message.into()));
262        self
263    }
264
265    /// Build the final prompt
266    pub fn build(&self) -> String {
267        let mut prompt = String::new();
268
269        // Add system message if present
270        if let Some(system) = &self.system_message {
271            prompt.push_str("System: ");
272            prompt.push_str(system);
273            prompt.push_str("\n\n");
274        }
275
276        // Add conversation messages
277        for (role, message) in &self.messages {
278            prompt.push_str(&format!("{}: ", role));
279            prompt.push_str(message);
280            prompt.push_str("\n\n");
281        }
282
283        prompt.trim_end().to_string()
284    }
285
286    /// Get system message and conversation separately (useful for API calls)
287    pub fn split(&self) -> (Option<String>, Vec<(Role, String)>) {
288        (self.system_message.clone(), self.messages.clone())
289    }
290}
291
292impl Default for RolePrompt {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298/// Instruction-based prompt builder
299///
300/// Constructs clear, structured prompts with explicit instructions,
301/// context, and constraints.
302#[derive(Debug, Clone)]
303pub struct InstructionPrompt {
304    task: String,
305    context: Vec<String>,
306    constraints: Vec<String>,
307    examples: Vec<String>,
308    format: Option<String>,
309}
310
311impl InstructionPrompt {
312    /// Create a new instruction prompt
313    pub fn new(task: impl Into<String>) -> Self {
314        Self {
315            task: task.into(),
316            context: Vec::new(),
317            constraints: Vec::new(),
318            examples: Vec::new(),
319            format: None,
320        }
321    }
322
323    /// Add context information
324    pub fn add_context(mut self, context: impl Into<String>) -> Self {
325        self.context.push(context.into());
326        self
327    }
328
329    /// Add a constraint
330    pub fn add_constraint(mut self, constraint: impl Into<String>) -> Self {
331        self.constraints.push(constraint.into());
332        self
333    }
334
335    /// Add an example
336    pub fn add_example(mut self, example: impl Into<String>) -> Self {
337        self.examples.push(example.into());
338        self
339    }
340
341    /// Specify expected output format
342    pub fn with_format(mut self, format: impl Into<String>) -> Self {
343        self.format = Some(format.into());
344        self
345    }
346
347    /// Build the final prompt
348    pub fn build(&self) -> String {
349        let mut prompt = String::new();
350
351        // Task
352        prompt.push_str("Task: ");
353        prompt.push_str(&self.task);
354        prompt.push_str("\n\n");
355
356        // Context
357        if !self.context.is_empty() {
358            prompt.push_str("Context:\n");
359            for (i, ctx) in self.context.iter().enumerate() {
360                prompt.push_str(&format!("{}. {}\n", i + 1, ctx));
361            }
362            prompt.push('\n');
363        }
364
365        // Constraints
366        if !self.constraints.is_empty() {
367            prompt.push_str("Constraints:\n");
368            for constraint in &self.constraints {
369                prompt.push_str("- ");
370                prompt.push_str(constraint);
371                prompt.push('\n');
372            }
373            prompt.push('\n');
374        }
375
376        // Examples
377        if !self.examples.is_empty() {
378            prompt.push_str("Examples:\n");
379            for (i, example) in self.examples.iter().enumerate() {
380                prompt.push_str(&format!("{}. {}\n", i + 1, example));
381            }
382            prompt.push('\n');
383        }
384
385        // Format
386        if let Some(format) = &self.format {
387            prompt.push_str("Expected Format: ");
388            prompt.push_str(format);
389            prompt.push_str("\n\n");
390        }
391
392        prompt.trim_end().to_string()
393    }
394}
395
396/// Common system prompts for different personas
397pub struct SystemPrompts;
398
399impl SystemPrompts {
400    /// Expert assistant persona
401    pub fn expert_assistant() -> String {
402        "You are an expert assistant with deep knowledge across multiple domains. \
403        Provide accurate, detailed, and well-reasoned responses. When uncertain, \
404        acknowledge limitations and suggest where to find more information."
405            .to_string()
406    }
407
408    /// Code assistant persona
409    pub fn code_assistant() -> String {
410        "You are an expert programming assistant. Provide clean, efficient, and \
411        well-documented code. Explain your reasoning and suggest best practices. \
412        Always consider edge cases and error handling."
413            .to_string()
414    }
415
416    /// Teacher persona
417    pub fn teacher() -> String {
418        "You are a patient and knowledgeable teacher. Break down complex topics \
419        into simple, understandable explanations. Use analogies and examples. \
420        Encourage learning by asking thought-provoking questions."
421            .to_string()
422    }
423
424    /// Analyst persona
425    pub fn analyst() -> String {
426        "You are a thorough analyst. Examine information critically, consider \
427        multiple perspectives, identify patterns and trends. Support your \
428        conclusions with evidence and clear reasoning."
429            .to_string()
430    }
431
432    /// Creative writer persona
433    pub fn creative_writer() -> String {
434        "You are a creative writer with a vivid imagination. Craft engaging \
435        narratives with rich descriptions and compelling characters. Pay attention \
436        to tone, pacing, and emotional resonance."
437            .to_string()
438    }
439
440    /// Concise responder persona
441    pub fn concise() -> String {
442        "You are a concise assistant. Provide brief, to-the-point responses. \
443        Focus on key information without unnecessary elaboration. Use clear, \
444        simple language."
445            .to_string()
446    }
447
448    /// Socratic questioner persona
449    pub fn socratic() -> String {
450        "You are a Socratic teacher. Guide users to discover answers through \
451        thoughtful questions. Encourage critical thinking and self-reflection. \
452        Help users develop their own understanding."
453            .to_string()
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    #[test]
462    fn test_few_shot_basic() {
463        let mut few_shot = FewShotPrompt::new("Classify sentiment:");
464        few_shot.add_example("Great!", "positive");
465        few_shot.add_example("Awful.", "negative");
466        few_shot.set_query("Amazing!");
467
468        let prompt = few_shot.build();
469        assert!(prompt.contains("Classify sentiment:"));
470        assert!(prompt.contains("Input: Great!"));
471        assert!(prompt.contains("Output: positive"));
472        assert!(prompt.contains("Input: Amazing!"));
473    }
474
475    #[test]
476    fn test_few_shot_custom_prefixes() {
477        let mut few_shot = FewShotPrompt::new("Test:");
478        few_shot
479            .with_prefixes("Q: ", "A: ")
480            .add_example("1+1", "2")
481            .set_query("2+2");
482
483        let prompt = few_shot.build();
484        assert!(prompt.contains("Q: 1+1"));
485        assert!(prompt.contains("A: 2"));
486    }
487
488    #[test]
489    fn test_chain_of_thought() {
490        let cot = ChainOfThought::new("What is 25% of 80?")
491            .with_instruction("Let's solve this step by step:");
492
493        let prompt = cot.build();
494        assert!(prompt.contains("Question: What is 25% of 80?"));
495        assert!(prompt.contains("Let's solve this step by step:"));
496    }
497
498    #[test]
499    fn test_chain_of_thought_with_examples() {
500        let cot = ChainOfThought::new("What is 15% of 60?").add_example(
501            "What is 10% of 50?",
502            "Step 1: Convert 10% to decimal: 0.10\nStep 2: Multiply: 50 × 0.10 = 5",
503        );
504
505        let prompt = cot.build();
506        assert!(prompt.contains("What is 10% of 50?"));
507        assert!(prompt.contains("Step 1"));
508        assert!(prompt.contains("What is 15% of 60?"));
509    }
510
511    #[test]
512    fn test_role_prompt() {
513        let prompt = RolePrompt::new()
514            .system("You are a helpful assistant.")
515            .user("Hello!")
516            .assistant("Hi! How can I help you?")
517            .user("Tell me a joke.");
518
519        let text = prompt.build();
520        assert!(text.contains("System: You are a helpful assistant."));
521        assert!(text.contains("User: Hello!"));
522        assert!(text.contains("Assistant: Hi! How can I help you?"));
523        assert!(text.contains("User: Tell me a joke."));
524    }
525
526    #[test]
527    fn test_role_prompt_split() {
528        let prompt = RolePrompt::new().system("Be helpful.").user("Hi");
529
530        let (system, messages) = prompt.split();
531        assert_eq!(system, Some("Be helpful.".to_string()));
532        assert_eq!(messages.len(), 1);
533        assert_eq!(messages[0].0, Role::User);
534    }
535
536    #[test]
537    fn test_instruction_prompt() {
538        let prompt = InstructionPrompt::new("Summarize this text")
539            .add_context("The text is a news article")
540            .add_constraint("Keep it under 100 words")
541            .add_constraint("Focus on key facts")
542            .with_format("Bullet points");
543
544        let text = prompt.build();
545        assert!(text.contains("Task: Summarize this text"));
546        assert!(text.contains("Context:"));
547        assert!(text.contains("news article"));
548        assert!(text.contains("Constraints:"));
549        assert!(text.contains("under 100 words"));
550        assert!(text.contains("Expected Format: Bullet points"));
551    }
552
553    #[test]
554    fn test_system_prompts() {
555        assert!(!SystemPrompts::expert_assistant().is_empty());
556        assert!(!SystemPrompts::code_assistant().is_empty());
557        assert!(!SystemPrompts::teacher().is_empty());
558        assert!(!SystemPrompts::analyst().is_empty());
559        assert!(!SystemPrompts::creative_writer().is_empty());
560        assert!(!SystemPrompts::concise().is_empty());
561        assert!(!SystemPrompts::socratic().is_empty());
562    }
563
564    #[test]
565    fn test_example_creation() {
566        let example = Example::new("input", "output");
567        assert_eq!(example.input, "input");
568        assert_eq!(example.output, "output");
569    }
570
571    #[test]
572    fn test_few_shot_add_multiple() {
573        let examples = vec![Example::new("a", "1"), Example::new("b", "2")];
574
575        let mut few_shot = FewShotPrompt::new("Test");
576        few_shot.add_examples(examples);
577
578        let prompt = few_shot.build();
579        assert!(prompt.contains("Input: a"));
580        assert!(prompt.contains("Input: b"));
581    }
582}