use crate::{
providers::LLMRequest,
shared::{Message, ToolDefinition},
skills::SkillIndexEntry,
state::AgentState,
};
#[derive(Debug, Default)]
pub(crate) struct PromptComposer;
pub(crate) struct PromptComposeInput {
pub(crate) model: String,
pub(crate) system_prompt: Option<String>,
pub(crate) history: Vec<Message>,
pub(crate) tools: Vec<ToolDefinition>,
pub(crate) skills: Vec<SkillIndexEntry>,
pub(crate) temperature: Option<f32>,
pub(crate) max_tokens: Option<u32>,
}
impl PromptComposer {
pub(crate) fn compose(&self, input: PromptComposeInput, _ctx: &AgentState) -> LLMRequest {
LLMRequest {
model: input.model,
system: render_system(input.system_prompt, input.skills),
messages: input.history,
tools: input.tools,
temperature: input.temperature,
max_tokens: input.max_tokens,
}
}
}
fn render_system(system_prompt: Option<String>, skills: Vec<SkillIndexEntry>) -> Option<String> {
let mut parts = Vec::new();
if let Some(system_prompt) = system_prompt {
if !system_prompt.trim().is_empty() {
parts.push(system_prompt);
}
}
if !skills.is_empty() {
let mut skill_index = String::from(
"Available skills are listed below. A skill is not active until you inspect its instructions. Use the readSkill tool to read only the skill instructions or skill resources that are relevant to the current task.\n\nAvailable skills:",
);
for skill in skills {
skill_index.push_str("\n- ");
skill_index.push_str(&skill.id);
skill_index.push_str(" (");
skill_index.push_str(&skill.name);
skill_index.push_str("): ");
skill_index.push_str(&skill.description);
}
parts.push(skill_index);
}
if parts.is_empty() {
None
} else {
Some(parts.join("\n\n"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compose_omits_system_without_skills() {
let req = PromptComposer.compose(
PromptComposeInput {
model: "test".to_string(),
system_prompt: None,
history: Vec::new(),
tools: Vec::new(),
skills: Vec::new(),
temperature: None,
max_tokens: None,
},
&AgentState::new(Some("s".to_string())),
);
assert!(req.system.is_none());
assert!(req.tools.is_empty());
}
#[test]
fn compose_renders_skill_index_without_tool_catalog() {
let req = PromptComposer.compose(
PromptComposeInput {
model: "test".to_string(),
system_prompt: Some("You are careful.".to_string()),
history: Vec::new(),
tools: vec![ToolDefinition {
name: "bash".to_string(),
desc: "execute bash".to_string(),
arguments: serde_json::json!({}),
}],
skills: vec![SkillIndexEntry {
id: "docs".to_string(),
name: "Documents".to_string(),
description: "Work with documents".to_string(),
}],
temperature: Some(0.4),
max_tokens: Some(1024),
},
&AgentState::new(Some("s".to_string())),
);
let system = req.system.unwrap();
assert!(system.contains("docs"));
assert!(system.contains("readSkill"));
assert!(system.contains("You are careful."));
assert!(!system.contains("execute bash"));
assert_eq!(req.tools.len(), 1);
assert_eq!(req.temperature, Some(0.4));
assert_eq!(req.max_tokens, Some(1024));
}
}