use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "content")]
pub enum KnowledgeFragment {
Logic {
instruction: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
steps: Vec<String>,
},
Guideline {
rule: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
anchors: Vec<Anchor>,
},
QualityStandard {
criteria: Vec<String>,
passing_grade: String,
},
ToolDefinition(serde_json::Value),
Text(String),
}
impl KnowledgeFragment {
pub fn to_prompt(&self) -> String {
match self {
KnowledgeFragment::Logic { instruction, steps } => {
let mut prompt = format!("## Logic\n{}\n", instruction);
if !steps.is_empty() {
prompt.push_str("\n### Steps:\n");
for (i, step) in steps.iter().enumerate() {
prompt.push_str(&format!("{}. {}\n", i + 1, step));
}
}
prompt
}
KnowledgeFragment::Guideline { rule, anchors } => {
let mut prompt = format!("## Guideline\n{}\n", rule);
if !anchors.is_empty() {
prompt.push_str("\n### Examples:\n");
for anchor in anchors {
prompt.push_str(&anchor.to_prompt());
prompt.push('\n');
}
}
prompt
}
KnowledgeFragment::QualityStandard {
criteria,
passing_grade,
} => {
let mut prompt = String::from("## Quality Standard\n\n### Criteria:\n");
for criterion in criteria {
prompt.push_str(&format!("- {}\n", criterion));
}
prompt.push_str(&format!("\n### Passing Grade:\n{}\n", passing_grade));
prompt
}
KnowledgeFragment::ToolDefinition(value) => {
format!(
"## Tool Definition\n```json\n{}\n```\n",
serde_json::to_string_pretty(value).unwrap_or_else(|_| "{}".to_string())
)
}
KnowledgeFragment::Text(text) => {
format!("{}\n", text)
}
}
}
pub fn type_label(&self) -> &'static str {
match self {
KnowledgeFragment::Logic { .. } => "Logic",
KnowledgeFragment::Guideline { .. } => "Guideline",
KnowledgeFragment::QualityStandard { .. } => "Quality",
KnowledgeFragment::ToolDefinition(_) => "Tool",
KnowledgeFragment::Text(_) => "Text",
}
}
pub fn summary(&self) -> String {
match self {
KnowledgeFragment::Logic { instruction, .. } => truncate(instruction, 50),
KnowledgeFragment::Guideline { rule, .. } => truncate(rule, 50),
KnowledgeFragment::QualityStandard { criteria, .. } => {
if criteria.is_empty() {
"No criteria".to_string()
} else {
format!("{} criteria", criteria.len())
}
}
KnowledgeFragment::ToolDefinition(_) => "Tool definition".to_string(),
KnowledgeFragment::Text(text) => truncate(text, 50),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Anchor {
pub context: String,
pub positive: String,
pub negative: String,
pub reason: String,
}
impl Anchor {
pub fn to_prompt(&self) -> String {
format!(
"**Context:** {}\n✅ **Good:** {}\n❌ **Bad:** {}\n**Why:** {}",
self.context, self.positive, self.negative, self.reason
)
}
}
fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logic_fragment_to_prompt() {
let fragment = KnowledgeFragment::Logic {
instruction: "Test instruction".to_string(),
steps: vec!["Step 1".to_string(), "Step 2".to_string()],
};
let prompt = fragment.to_prompt();
assert!(prompt.contains("## Logic"));
assert!(prompt.contains("Test instruction"));
assert!(prompt.contains("1. Step 1"));
assert!(prompt.contains("2. Step 2"));
}
#[test]
fn test_guideline_fragment_to_prompt() {
let anchor = Anchor {
context: "Test context".to_string(),
positive: "Good example".to_string(),
negative: "Bad example".to_string(),
reason: "Because".to_string(),
};
let fragment = KnowledgeFragment::Guideline {
rule: "Test rule".to_string(),
anchors: vec![anchor],
};
let prompt = fragment.to_prompt();
assert!(prompt.contains("## Guideline"));
assert!(prompt.contains("Test rule"));
assert!(prompt.contains("Good example"));
assert!(prompt.contains("Bad example"));
}
#[test]
fn test_fragment_type_label() {
let logic = KnowledgeFragment::Logic {
instruction: "Test".to_string(),
steps: vec![],
};
assert_eq!(logic.type_label(), "Logic");
let text = KnowledgeFragment::Text("Test".to_string());
assert_eq!(text.type_label(), "Text");
}
#[test]
fn test_truncate() {
assert_eq!(truncate("short", 10), "short");
assert_eq!(truncate("this is a very long text", 10), "this is...");
}
}