fix(skills): inject skill list into system prompt for LLM awareness
Problem: Agent could not invoke appropriate skills when user asked about financial reports because LLM didn't know which skills were available. Root causes: 1. System prompt lacked available skill list 2. SkillManifest struct missing 'triggers' field 3. SKILL.md loader not parsing triggers list 4. "财报" keyword not matching "财务报告" trigger Changes: - Add triggers field to SkillManifest struct - Parse triggers list from SKILL.md frontmatter - Inject skill list into system prompt in kernel.rs - Add "财报", "财务数据", "盈利", "营收" triggers to finance-tracker - Add "财报分析" trigger to analytics-reporter - Document fix in troubleshooting.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,47 @@ impl Kernel {
|
||||
tools
|
||||
}
|
||||
|
||||
/// Build a system prompt with skill information injected
|
||||
fn build_system_prompt_with_skills(&self, base_prompt: Option<&String>) -> String {
|
||||
// Get skill list synchronously (we're in sync context)
|
||||
let skills = futures::executor::block_on(self.skills.list());
|
||||
|
||||
let mut prompt = base_prompt
|
||||
.map(|p| p.clone())
|
||||
.unwrap_or_else(|| "You are a helpful AI assistant.".to_string());
|
||||
|
||||
// Inject skill information
|
||||
if !skills.is_empty() {
|
||||
prompt.push_str("\n\n## Available Skills\n\n");
|
||||
prompt.push_str("You have access to the following skills that can help with specific tasks. ");
|
||||
prompt.push_str("Use the `execute_skill` tool with the skill_id to invoke them:\n\n");
|
||||
|
||||
for skill in skills {
|
||||
prompt.push_str(&format!(
|
||||
"- **{}**: {}",
|
||||
skill.id.as_str(),
|
||||
skill.description
|
||||
));
|
||||
|
||||
// Add trigger words if available
|
||||
if !skill.triggers.is_empty() {
|
||||
prompt.push_str(&format!(
|
||||
" (Triggers: {})",
|
||||
skill.triggers.join(", ")
|
||||
));
|
||||
}
|
||||
prompt.push('\n');
|
||||
}
|
||||
|
||||
prompt.push_str("\n### When to use skills:\n");
|
||||
prompt.push_str("- When the user's request matches a skill's trigger words\n");
|
||||
prompt.push_str("- When you need specialized expertise for a task\n");
|
||||
prompt.push_str("- When the task would benefit from a structured workflow\n");
|
||||
}
|
||||
|
||||
prompt
|
||||
}
|
||||
|
||||
/// Spawn a new agent
|
||||
pub async fn spawn_agent(&self, config: AgentConfig) -> Result<AgentId> {
|
||||
let id = config.id;
|
||||
@@ -197,12 +238,9 @@ impl Kernel {
|
||||
.with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
|
||||
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
|
||||
|
||||
// Add system prompt if configured
|
||||
let loop_runner = if let Some(ref prompt) = agent_config.system_prompt {
|
||||
loop_runner.with_system_prompt(prompt)
|
||||
} else {
|
||||
loop_runner
|
||||
};
|
||||
// Build system prompt with skill information injected
|
||||
let system_prompt = self.build_system_prompt_with_skills(agent_config.system_prompt.as_ref());
|
||||
let loop_runner = loop_runner.with_system_prompt(&system_prompt);
|
||||
|
||||
// Run the loop
|
||||
let result = loop_runner.run(session_id, message).await?;
|
||||
@@ -243,12 +281,9 @@ impl Kernel {
|
||||
.with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
|
||||
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
|
||||
|
||||
// Add system prompt if configured
|
||||
let loop_runner = if let Some(ref prompt) = agent_config.system_prompt {
|
||||
loop_runner.with_system_prompt(prompt)
|
||||
} else {
|
||||
loop_runner
|
||||
};
|
||||
// Build system prompt with skill information injected
|
||||
let system_prompt = self.build_system_prompt_with_skills(agent_config.system_prompt.as_ref());
|
||||
let loop_runner = loop_runner.with_system_prompt(&system_prompt);
|
||||
|
||||
// Run with streaming
|
||||
loop_runner.run_streaming(session_id, message).await
|
||||
|
||||
@@ -41,6 +41,8 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
||||
let mut mode = SkillMode::PromptOnly;
|
||||
let mut capabilities = Vec::new();
|
||||
let mut tags = Vec::new();
|
||||
let mut triggers = Vec::new();
|
||||
let mut in_triggers_list = false;
|
||||
|
||||
// Parse frontmatter if present
|
||||
if content.starts_with("---") {
|
||||
@@ -51,6 +53,15 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
||||
if line.is_empty() || line == "---" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle triggers list items
|
||||
if in_triggers_list && line.starts_with("- ") {
|
||||
triggers.push(line[2..].trim().trim_matches('"').to_string());
|
||||
continue;
|
||||
} else {
|
||||
in_triggers_list = false;
|
||||
}
|
||||
|
||||
if let Some((key, value)) = line.split_once(':') {
|
||||
let key = key.trim();
|
||||
let value = value.trim().trim_matches('"');
|
||||
@@ -69,6 +80,16 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
||||
.map(|s| s.trim().to_string())
|
||||
.collect();
|
||||
}
|
||||
"triggers" => {
|
||||
// Check if it's a list on next lines or inline
|
||||
if value.is_empty() {
|
||||
in_triggers_list = true;
|
||||
} else {
|
||||
triggers = value.split(',')
|
||||
.map(|s| s.trim().trim_matches('"').to_string())
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -137,6 +158,7 @@ pub fn parse_skill_md(content: &str) -> Result<SkillManifest> {
|
||||
input_schema: None,
|
||||
output_schema: None,
|
||||
tags,
|
||||
triggers,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
@@ -159,6 +181,7 @@ pub fn parse_skill_toml(content: &str) -> Result<SkillManifest> {
|
||||
let mut mode = "prompt_only".to_string();
|
||||
let mut capabilities = Vec::new();
|
||||
let mut tags = Vec::new();
|
||||
let mut triggers = Vec::new();
|
||||
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
@@ -189,6 +212,13 @@ pub fn parse_skill_toml(content: &str) -> Result<SkillManifest> {
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
}
|
||||
"triggers" => {
|
||||
let value = value.trim_start_matches('[').trim_end_matches(']');
|
||||
triggers = value.split(',')
|
||||
.map(|s| s.trim().trim_matches('"').to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -215,6 +245,7 @@ pub fn parse_skill_toml(content: &str) -> Result<SkillManifest> {
|
||||
input_schema: None,
|
||||
output_schema: None,
|
||||
tags,
|
||||
triggers,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ pub struct SkillManifest {
|
||||
/// Tags for categorization
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
/// Trigger words for skill activation
|
||||
#[serde(default)]
|
||||
pub triggers: Vec<String>,
|
||||
/// Whether the skill is enabled
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
|
||||
Reference in New Issue
Block a user