llm_toolkit_expertise/
fragment.rs

1//! Knowledge fragment types and definitions.
2//!
3//! This module defines the core knowledge representation units that can be
4//! composed into expertise profiles.
5
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9/// KnowledgeFragment: Minimal unit of knowledge
10///
11/// Represents different types of knowledge that can be incorporated
12/// into an agent's expertise.
13#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
14#[serde(tag = "type", content = "content")]
15pub enum KnowledgeFragment {
16    /// Thinking logic and procedures
17    Logic {
18        /// High-level instruction
19        instruction: String,
20        /// Chain-of-Thought steps
21        #[serde(default, skip_serializing_if = "Vec::is_empty")]
22        steps: Vec<String>,
23    },
24
25    /// Behavioral guidelines with anchoring examples
26    Guideline {
27        /// The rule or guideline statement
28        rule: String,
29        /// Anchoring examples (positive/negative pairs)
30        #[serde(default, skip_serializing_if = "Vec::is_empty")]
31        anchors: Vec<Anchor>,
32    },
33
34    /// Quality evaluation criteria
35    QualityStandard {
36        /// List of evaluation criteria
37        criteria: Vec<String>,
38        /// Description of passing grade
39        passing_grade: String,
40    },
41
42    /// Tool definition (interface)
43    ToolDefinition(serde_json::Value),
44
45    /// Free-form text knowledge
46    Text(String),
47}
48
49impl KnowledgeFragment {
50    /// Convert the fragment to a prompt string
51    pub fn to_prompt(&self) -> String {
52        match self {
53            KnowledgeFragment::Logic { instruction, steps } => {
54                let mut prompt = format!("## Logic\n{}\n", instruction);
55                if !steps.is_empty() {
56                    prompt.push_str("\n### Steps:\n");
57                    for (i, step) in steps.iter().enumerate() {
58                        prompt.push_str(&format!("{}. {}\n", i + 1, step));
59                    }
60                }
61                prompt
62            }
63            KnowledgeFragment::Guideline { rule, anchors } => {
64                let mut prompt = format!("## Guideline\n{}\n", rule);
65                if !anchors.is_empty() {
66                    prompt.push_str("\n### Examples:\n");
67                    for anchor in anchors {
68                        prompt.push_str(&anchor.to_prompt());
69                        prompt.push('\n');
70                    }
71                }
72                prompt
73            }
74            KnowledgeFragment::QualityStandard {
75                criteria,
76                passing_grade,
77            } => {
78                let mut prompt = String::from("## Quality Standard\n\n### Criteria:\n");
79                for criterion in criteria {
80                    prompt.push_str(&format!("- {}\n", criterion));
81                }
82                prompt.push_str(&format!("\n### Passing Grade:\n{}\n", passing_grade));
83                prompt
84            }
85            KnowledgeFragment::ToolDefinition(value) => {
86                format!(
87                    "## Tool Definition\n```json\n{}\n```\n",
88                    serde_json::to_string_pretty(value).unwrap_or_else(|_| "{}".to_string())
89                )
90            }
91            KnowledgeFragment::Text(text) => {
92                format!("{}\n", text)
93            }
94        }
95    }
96
97    /// Get a short type label for visualization
98    pub fn type_label(&self) -> &'static str {
99        match self {
100            KnowledgeFragment::Logic { .. } => "Logic",
101            KnowledgeFragment::Guideline { .. } => "Guideline",
102            KnowledgeFragment::QualityStandard { .. } => "Quality",
103            KnowledgeFragment::ToolDefinition(_) => "Tool",
104            KnowledgeFragment::Text(_) => "Text",
105        }
106    }
107
108    /// Get a brief summary of the fragment content
109    pub fn summary(&self) -> String {
110        match self {
111            KnowledgeFragment::Logic { instruction, .. } => truncate(instruction, 50),
112            KnowledgeFragment::Guideline { rule, .. } => truncate(rule, 50),
113            KnowledgeFragment::QualityStandard { criteria, .. } => {
114                if criteria.is_empty() {
115                    "No criteria".to_string()
116                } else {
117                    format!("{} criteria", criteria.len())
118                }
119            }
120            KnowledgeFragment::ToolDefinition(_) => "Tool definition".to_string(),
121            KnowledgeFragment::Text(text) => truncate(text, 50),
122        }
123    }
124}
125
126/// Anchor: Positive/negative example pair for behavioral anchoring
127///
128/// Provides concrete examples to establish standards and expectations.
129#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
130pub struct Anchor {
131    /// Context or scenario
132    pub context: String,
133    /// Positive example (ideal form)
134    pub positive: String,
135    /// Negative example (form to avoid)
136    pub negative: String,
137    /// Explanation of why
138    pub reason: String,
139}
140
141impl Anchor {
142    /// Convert anchor to prompt format
143    pub fn to_prompt(&self) -> String {
144        format!(
145            "**Context:** {}\n✅ **Good:** {}\n❌ **Bad:** {}\n**Why:** {}",
146            self.context, self.positive, self.negative, self.reason
147        )
148    }
149}
150
151/// Helper function to truncate text for summaries
152fn truncate(s: &str, max_len: usize) -> String {
153    if s.len() <= max_len {
154        s.to_string()
155    } else {
156        format!("{}...", &s[..max_len.saturating_sub(3)])
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_logic_fragment_to_prompt() {
166        let fragment = KnowledgeFragment::Logic {
167            instruction: "Test instruction".to_string(),
168            steps: vec!["Step 1".to_string(), "Step 2".to_string()],
169        };
170        let prompt = fragment.to_prompt();
171        assert!(prompt.contains("## Logic"));
172        assert!(prompt.contains("Test instruction"));
173        assert!(prompt.contains("1. Step 1"));
174        assert!(prompt.contains("2. Step 2"));
175    }
176
177    #[test]
178    fn test_guideline_fragment_to_prompt() {
179        let anchor = Anchor {
180            context: "Test context".to_string(),
181            positive: "Good example".to_string(),
182            negative: "Bad example".to_string(),
183            reason: "Because".to_string(),
184        };
185        let fragment = KnowledgeFragment::Guideline {
186            rule: "Test rule".to_string(),
187            anchors: vec![anchor],
188        };
189        let prompt = fragment.to_prompt();
190        assert!(prompt.contains("## Guideline"));
191        assert!(prompt.contains("Test rule"));
192        assert!(prompt.contains("Good example"));
193        assert!(prompt.contains("Bad example"));
194    }
195
196    #[test]
197    fn test_fragment_type_label() {
198        let logic = KnowledgeFragment::Logic {
199            instruction: "Test".to_string(),
200            steps: vec![],
201        };
202        assert_eq!(logic.type_label(), "Logic");
203
204        let text = KnowledgeFragment::Text("Test".to_string());
205        assert_eq!(text.type_label(), "Text");
206    }
207
208    #[test]
209    fn test_truncate() {
210        assert_eq!(truncate("short", 10), "short");
211        assert_eq!(truncate("this is a very long text", 10), "this is...");
212    }
213}