feat: 重构技能事件处理逻辑,移除 SkillEventSink,添加 SkillActivateTool 模块以优化技能激活流程
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
396504dffb
commit
90e44950cb
@ -2,7 +2,6 @@ use crate::bus::ChatMessage;
|
|||||||
use crate::bus::message::ToolMessageState;
|
use crate::bus::message::ToolMessageState;
|
||||||
use crate::config::LLMProviderConfig;
|
use crate::config::LLMProviderConfig;
|
||||||
use crate::domain::messages::{ContentBlock, ToolCall};
|
use crate::domain::messages::{ContentBlock, ToolCall};
|
||||||
use crate::domain::tools::Tool;
|
|
||||||
use crate::observability::{
|
use crate::observability::{
|
||||||
Observer, ObserverEvent, ToolExecutionOutcome, ToolExecutionState, truncate_args,
|
Observer, ObserverEvent, ToolExecutionOutcome, ToolExecutionState, truncate_args,
|
||||||
};
|
};
|
||||||
@ -297,7 +296,6 @@ pub struct AgentLoop {
|
|||||||
provider: Box<dyn LLMProvider>,
|
provider: Box<dyn LLMProvider>,
|
||||||
tools: Arc<ToolRegistry>,
|
tools: Arc<ToolRegistry>,
|
||||||
skills: Arc<dyn SkillProvider>,
|
skills: Arc<dyn SkillProvider>,
|
||||||
skill_event_sink: Option<Arc<dyn SkillEventSink>>,
|
|
||||||
tool_context: ToolContext,
|
tool_context: ToolContext,
|
||||||
observer: Option<Arc<dyn Observer>>,
|
observer: Option<Arc<dyn Observer>>,
|
||||||
emitted_message_handler: Option<Arc<dyn EmittedMessageHandler>>,
|
emitted_message_handler: Option<Arc<dyn EmittedMessageHandler>>,
|
||||||
@ -315,25 +313,8 @@ pub trait EmittedMessageHandler: Send + Sync + 'static {
|
|||||||
async fn handle(&self, message: ChatMessage);
|
async fn handle(&self, message: ChatMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SkillEvent {
|
|
||||||
pub event_type: String,
|
|
||||||
pub skill_name: Option<String>,
|
|
||||||
pub payload: serde_json::Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait SkillEventSink: Send + Sync + 'static {
|
|
||||||
fn record_skill_event(&self, event: SkillEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait SkillProvider: Send + Sync + 'static {
|
pub trait SkillProvider: Send + Sync + 'static {
|
||||||
fn system_index_prompt(&self) -> Option<String>;
|
fn system_index_prompt(&self) -> Option<String>;
|
||||||
|
|
||||||
fn skill_tool_definition(&self) -> Option<Tool>;
|
|
||||||
|
|
||||||
fn activation_payload(&self, name: &str) -> Result<String, String>;
|
|
||||||
|
|
||||||
fn activation_event_payload(&self, name: &str) -> Result<serde_json::Value, String>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -343,18 +324,6 @@ impl SkillProvider for EmptySkillProvider {
|
|||||||
fn system_index_prompt(&self) -> Option<String> {
|
fn system_index_prompt(&self) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skill_tool_definition(&self) -> Option<Tool> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn activation_payload(&self, name: &str) -> Result<String, String> {
|
|
||||||
Err(format!("skill '{}' not found", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn activation_event_payload(&self, name: &str) -> Result<serde_json::Value, String> {
|
|
||||||
Err(format!("skill '{}' not found", name))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentLoop {
|
impl AgentLoop {
|
||||||
@ -368,7 +337,6 @@ impl AgentLoop {
|
|||||||
provider,
|
provider,
|
||||||
tools: Arc::new(ToolRegistry::new()),
|
tools: Arc::new(ToolRegistry::new()),
|
||||||
skills: Arc::new(EmptySkillProvider),
|
skills: Arc::new(EmptySkillProvider),
|
||||||
skill_event_sink: None,
|
|
||||||
tool_context: ToolContext::default(),
|
tool_context: ToolContext::default(),
|
||||||
observer: None,
|
observer: None,
|
||||||
emitted_message_handler: None,
|
emitted_message_handler: None,
|
||||||
@ -389,7 +357,6 @@ impl AgentLoop {
|
|||||||
provider,
|
provider,
|
||||||
tools,
|
tools,
|
||||||
skills: Arc::new(EmptySkillProvider),
|
skills: Arc::new(EmptySkillProvider),
|
||||||
skill_event_sink: None,
|
|
||||||
tool_context: ToolContext::default(),
|
tool_context: ToolContext::default(),
|
||||||
observer: None,
|
observer: None,
|
||||||
emitted_message_handler: None,
|
emitted_message_handler: None,
|
||||||
@ -411,7 +378,6 @@ impl AgentLoop {
|
|||||||
provider,
|
provider,
|
||||||
tools,
|
tools,
|
||||||
skills,
|
skills,
|
||||||
skill_event_sink: None,
|
|
||||||
tool_context: ToolContext::default(),
|
tool_context: ToolContext::default(),
|
||||||
observer: None,
|
observer: None,
|
||||||
emitted_message_handler: None,
|
emitted_message_handler: None,
|
||||||
@ -419,11 +385,6 @@ impl AgentLoop {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_skill_event_sink(mut self, sink: Arc<dyn SkillEventSink>) -> Self {
|
|
||||||
self.skill_event_sink = Some(sink);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_tool_context(mut self, context: ToolContext) -> Self {
|
pub fn with_tool_context(mut self, context: ToolContext) -> Self {
|
||||||
self.tool_context = context;
|
self.tool_context = context;
|
||||||
self
|
self
|
||||||
@ -479,10 +440,7 @@ impl AgentLoop {
|
|||||||
messages_for_llm.extend(messages.iter().map(chat_message_to_llm_message));
|
messages_for_llm.extend(messages.iter().map(chat_message_to_llm_message));
|
||||||
|
|
||||||
// Build request
|
// Build request
|
||||||
let mut tool_defs = self.tools.get_definitions();
|
let tool_defs = self.tools.get_definitions();
|
||||||
if let Some(skill_tool) = self.skills.skill_tool_definition() {
|
|
||||||
tool_defs.push(skill_tool);
|
|
||||||
}
|
|
||||||
let tools = if tool_defs.is_empty() {
|
let tools = if tool_defs.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@ -818,46 +776,6 @@ impl AgentLoop {
|
|||||||
async fn execute_tool_internal(&self, tool_call: &ToolCall) -> ToolExecutionOutcome {
|
async fn execute_tool_internal(&self, tool_call: &ToolCall) -> ToolExecutionOutcome {
|
||||||
let normalized_arguments = normalize_tool_arguments(&tool_call.arguments);
|
let normalized_arguments = normalize_tool_arguments(&tool_call.arguments);
|
||||||
|
|
||||||
if tool_call.name == "skill_activate" {
|
|
||||||
let skill_name = match normalized_arguments.get("name").and_then(|v| v.as_str()) {
|
|
||||||
Some(name) if !name.trim().is_empty() => name,
|
|
||||||
_ => {
|
|
||||||
self.record_skill_event(
|
|
||||||
"activation_failed",
|
|
||||||
None,
|
|
||||||
serde_json::json!({
|
|
||||||
"reason": "missing_name",
|
|
||||||
"arguments": normalized_arguments,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return ToolExecutionOutcome::failure(
|
|
||||||
"Error: Missing required parameter: name".to_string(),
|
|
||||||
Some("Missing required parameter: name".to_string()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return match self.skills.activation_payload(skill_name) {
|
|
||||||
Ok(output) => {
|
|
||||||
if let Ok(payload) = self.skills.activation_event_payload(skill_name) {
|
|
||||||
self.record_skill_event("activated", Some(skill_name), payload);
|
|
||||||
}
|
|
||||||
ToolExecutionOutcome::success(output)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.record_skill_event(
|
|
||||||
"activation_failed",
|
|
||||||
Some(skill_name),
|
|
||||||
serde_json::json!({
|
|
||||||
"reason": err,
|
|
||||||
"arguments": normalized_arguments,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
ToolExecutionOutcome::failure(format!("Error: {}", err), Some(err))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let tool = match self.tools.get(&tool_call.name) {
|
let tool = match self.tools.get(&tool_call.name) {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
@ -906,23 +824,6 @@ impl AgentLoop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record_skill_event(
|
|
||||||
&self,
|
|
||||||
event_type: &str,
|
|
||||||
skill_name: Option<&str>,
|
|
||||||
payload: serde_json::Value,
|
|
||||||
) {
|
|
||||||
let Some(sink) = self.skill_event_sink.as_ref() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
sink.record_skill_event(SkillEvent {
|
|
||||||
event_type: event_type.to_string(),
|
|
||||||
skill_name: skill_name.map(str::to_string),
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -931,25 +832,6 @@ mod tests {
|
|||||||
use crate::observability::{MultiObserver, Observer};
|
use crate::observability::{MultiObserver, Observer};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
fn test_provider_config() -> LLMProviderConfig {
|
|
||||||
LLMProviderConfig {
|
|
||||||
provider_type: "openai".to_string(),
|
|
||||||
name: "test".to_string(),
|
|
||||||
base_url: "http://localhost".to_string(),
|
|
||||||
api_key: "test-key".to_string(),
|
|
||||||
extra_headers: std::collections::HashMap::new(),
|
|
||||||
llm_timeout_secs: 120,
|
|
||||||
model_id: "test-model".to_string(),
|
|
||||||
temperature: Some(0.0),
|
|
||||||
max_tokens: Some(32),
|
|
||||||
context_window_tokens: None,
|
|
||||||
model_extra: std::collections::HashMap::new(),
|
|
||||||
max_tool_iterations: 1,
|
|
||||||
tool_result_max_chars: 20_000,
|
|
||||||
context_tool_result_trim_chars: 20_000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TestObserver {
|
struct TestObserver {
|
||||||
events: std::sync::Mutex<Vec<ObserverEvent>>,
|
events: std::sync::Mutex<Vec<ObserverEvent>>,
|
||||||
}
|
}
|
||||||
@ -962,24 +844,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TestSkillEventSink {
|
|
||||||
events: std::sync::Mutex<Vec<SkillEvent>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestSkillEventSink {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
events: std::sync::Mutex::new(Vec::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SkillEventSink for TestSkillEventSink {
|
|
||||||
fn record_skill_event(&self, event: SkillEvent) {
|
|
||||||
self.events.lock().unwrap().push(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Observer for TestObserver {
|
impl Observer for TestObserver {
|
||||||
fn record_event(&self, event: &ObserverEvent) {
|
fn record_event(&self, event: &ObserverEvent) {
|
||||||
self.events.lock().unwrap().push(event.clone());
|
self.events.lock().unwrap().push(event.clone());
|
||||||
@ -1006,26 +870,6 @@ mod tests {
|
|||||||
assert_eq!(multi.len(), 1);
|
assert_eq!(multi.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_skill_events_are_emitted_to_sink() {
|
|
||||||
let sink = Arc::new(TestSkillEventSink::new());
|
|
||||||
let agent = AgentLoop::new(test_provider_config())
|
|
||||||
.unwrap()
|
|
||||||
.with_skill_event_sink(sink.clone());
|
|
||||||
|
|
||||||
agent.record_skill_event(
|
|
||||||
"activated",
|
|
||||||
Some("demo"),
|
|
||||||
serde_json::json!({ "source": "test" }),
|
|
||||||
);
|
|
||||||
|
|
||||||
let events = sink.events.lock().unwrap();
|
|
||||||
assert_eq!(events.len(), 1);
|
|
||||||
assert_eq!(events[0].event_type, "activated");
|
|
||||||
assert_eq!(events[0].skill_name.as_deref(), Some("demo"));
|
|
||||||
assert_eq!(events[0].payload, serde_json::json!({ "source": "test" }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_should_execute_in_parallel_single_tool() {
|
fn test_should_execute_in_parallel_single_tool() {
|
||||||
// Would need a proper setup with AgentLoop to test fully
|
// Would need a proper setup with AgentLoop to test fully
|
||||||
|
|||||||
@ -2,7 +2,6 @@ pub mod agent_loop;
|
|||||||
pub mod context_compressor;
|
pub mod context_compressor;
|
||||||
|
|
||||||
pub use agent_loop::{
|
pub use agent_loop::{
|
||||||
AgentError, AgentLoop, AgentProcessResult, EmittedMessageHandler, SkillEvent, SkillEventSink,
|
AgentError, AgentLoop, AgentProcessResult, EmittedMessageHandler, SkillProvider,
|
||||||
SkillProvider,
|
|
||||||
};
|
};
|
||||||
pub use context_compressor::ContextCompressor;
|
pub use context_compressor::ContextCompressor;
|
||||||
|
|||||||
@ -2,16 +2,13 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crate::agent::{AgentError, AgentLoop, SkillProvider};
|
use crate::agent::{AgentError, AgentLoop, SkillProvider};
|
||||||
use crate::config::LLMProviderConfig;
|
use crate::config::LLMProviderConfig;
|
||||||
use crate::storage::{SessionStore, persistent_session_id};
|
use crate::storage::persistent_session_id;
|
||||||
use crate::tools::{ToolContext, ToolRegistry};
|
use crate::tools::{ToolContext, ToolRegistry};
|
||||||
|
|
||||||
use super::skill_event_sink::PersistentSkillEventSink;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct AgentFactory {
|
pub(crate) struct AgentFactory {
|
||||||
tools: Arc<ToolRegistry>,
|
tools: Arc<ToolRegistry>,
|
||||||
skills: Arc<dyn SkillProvider>,
|
skills: Arc<dyn SkillProvider>,
|
||||||
store: Arc<SessionStore>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct AgentBuildRequest<'a> {
|
pub(crate) struct AgentBuildRequest<'a> {
|
||||||
@ -23,16 +20,8 @@ pub(crate) struct AgentBuildRequest<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentFactory {
|
impl AgentFactory {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(tools: Arc<ToolRegistry>, skills: Arc<dyn SkillProvider>) -> Self {
|
||||||
tools: Arc<ToolRegistry>,
|
Self { tools, skills }
|
||||||
skills: Arc<dyn SkillProvider>,
|
|
||||||
store: Arc<SessionStore>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
tools,
|
|
||||||
skills,
|
|
||||||
store,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn create(&self, request: AgentBuildRequest<'_>) -> Result<AgentLoop, AgentError> {
|
pub(crate) fn create(&self, request: AgentBuildRequest<'_>) -> Result<AgentLoop, AgentError> {
|
||||||
@ -43,14 +32,7 @@ impl AgentFactory {
|
|||||||
self.skills.clone(),
|
self.skills.clone(),
|
||||||
)
|
)
|
||||||
.map(|agent| {
|
.map(|agent| {
|
||||||
let skill_event_sink = Arc::new(PersistentSkillEventSink::new(
|
agent.with_tool_context(ToolContext {
|
||||||
self.store.clone(),
|
|
||||||
session_id.clone(),
|
|
||||||
));
|
|
||||||
|
|
||||||
agent
|
|
||||||
.with_skill_event_sink(skill_event_sink)
|
|
||||||
.with_tool_context(ToolContext {
|
|
||||||
channel_name: Some(request.channel_name.to_string()),
|
channel_name: Some(request.channel_name.to_string()),
|
||||||
sender_id: request.sender_id.map(str::to_string),
|
sender_id: request.sender_id.map(str::to_string),
|
||||||
chat_id: Some(request.chat_id.to_string()),
|
chat_id: Some(request.chat_id.to_string()),
|
||||||
|
|||||||
@ -20,7 +20,6 @@ pub mod session_history;
|
|||||||
pub mod session_lifecycle;
|
pub mod session_lifecycle;
|
||||||
pub mod session_message_service;
|
pub mod session_message_service;
|
||||||
pub mod session_pool;
|
pub mod session_pool;
|
||||||
pub mod skill_event_sink;
|
|
||||||
pub mod tool_registry_factory;
|
pub mod tool_registry_factory;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|
||||||
|
|||||||
@ -104,7 +104,7 @@ impl Session {
|
|||||||
store: Arc<SessionStore>,
|
store: Arc<SessionStore>,
|
||||||
agent_prompt_reinject_every: u64,
|
agent_prompt_reinject_every: u64,
|
||||||
) -> Result<Self, AgentError> {
|
) -> Result<Self, AgentError> {
|
||||||
let agent_factory = AgentFactory::new(tools, skills.clone(), store.clone());
|
let agent_factory = AgentFactory::new(tools, skills.clone());
|
||||||
let prompt_injector = PromptInjector::new(store.clone(), agent_prompt_reinject_every);
|
let prompt_injector = PromptInjector::new(store.clone(), agent_prompt_reinject_every);
|
||||||
Self::with_factories(
|
Self::with_factories(
|
||||||
channel_name,
|
channel_name,
|
||||||
@ -370,7 +370,7 @@ impl SessionManager {
|
|||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
let agent_factory = AgentFactory::new(tools.clone(), skills.clone(), store.clone());
|
let agent_factory = AgentFactory::new(tools.clone(), skills.clone());
|
||||||
let prompt_injector = PromptInjector::new(store.clone(), agent_prompt_reinject_every);
|
let prompt_injector = PromptInjector::new(store.clone(), agent_prompt_reinject_every);
|
||||||
let session_factory = SessionFactory::new(
|
let session_factory = SessionFactory::new(
|
||||||
provider_config.clone(),
|
provider_config.clone(),
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::agent::{SkillEvent, SkillEventSink};
|
|
||||||
use crate::storage::SkillEventRepository;
|
|
||||||
|
|
||||||
pub(crate) struct PersistentSkillEventSink {
|
|
||||||
events: Arc<dyn SkillEventRepository>,
|
|
||||||
session_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PersistentSkillEventSink {
|
|
||||||
pub(crate) fn new(events: Arc<dyn SkillEventRepository>, session_id: String) -> Self {
|
|
||||||
Self { events, session_id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SkillEventSink for PersistentSkillEventSink {
|
|
||||||
fn record_skill_event(&self, event: SkillEvent) {
|
|
||||||
if let Err(err) = self.events.append_skill_event(
|
|
||||||
Some(&self.session_id),
|
|
||||||
&event.event_type,
|
|
||||||
event.skill_name.as_deref(),
|
|
||||||
&event.payload,
|
|
||||||
) {
|
|
||||||
tracing::warn!(
|
|
||||||
error = %err,
|
|
||||||
event_type = %event.event_type,
|
|
||||||
"Failed to record skill event"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,8 +5,8 @@ use crate::skills::SkillRuntime;
|
|||||||
use crate::storage::SessionStore;
|
use crate::storage::SessionStore;
|
||||||
use crate::tools::{
|
use crate::tools::{
|
||||||
BashTool, CalculatorTool, FileEditTool, FileReadTool, FileWriteTool, HttpRequestTool,
|
BashTool, CalculatorTool, FileEditTool, FileReadTool, FileWriteTool, HttpRequestTool,
|
||||||
MemoryManageTool, MemorySearchTool, SchedulerManageTool, SkillListTool, SkillManageTool,
|
MemoryManageTool, MemorySearchTool, SchedulerManageTool, SkillActivateTool, SkillListTool,
|
||||||
TimeTool, ToolRegistry, WebFetchTool,
|
SkillManageTool, TimeTool, ToolRegistry, WebFetchTool,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) struct ToolRegistryFactory {
|
pub(crate) struct ToolRegistryFactory {
|
||||||
@ -44,6 +44,10 @@ impl ToolRegistryFactory {
|
|||||||
self.store.clone(),
|
self.store.clone(),
|
||||||
self.known_agents.clone(),
|
self.known_agents.clone(),
|
||||||
));
|
));
|
||||||
|
registry.register(SkillActivateTool::new(
|
||||||
|
self.skills.clone(),
|
||||||
|
self.store.clone(),
|
||||||
|
));
|
||||||
registry.register(SkillListTool::new(self.skills.clone()));
|
registry.register(SkillListTool::new(self.skills.clone()));
|
||||||
registry.register(SkillManageTool::new(self.skills.clone()));
|
registry.register(SkillManageTool::new(self.skills.clone()));
|
||||||
registry.register(BashTool::new());
|
registry.register(BashTool::new());
|
||||||
|
|||||||
@ -6,7 +6,6 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use crate::config::SkillsConfig;
|
use crate::config::SkillsConfig;
|
||||||
use crate::domain::tools::{Tool, ToolFunction};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Skill {
|
pub struct Skill {
|
||||||
@ -120,13 +119,6 @@ impl SkillRuntime {
|
|||||||
.offered_event_payload()
|
.offered_event_payload()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn skill_tool_definition(&self) -> Option<Tool> {
|
|
||||||
self.catalog
|
|
||||||
.read()
|
|
||||||
.expect("skills rwlock poisoned")
|
|
||||||
.skill_tool_definition()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn activation_payload(&self, name: &str) -> Result<String, String> {
|
pub fn activation_payload(&self, name: &str) -> Result<String, String> {
|
||||||
self.catalog
|
self.catalog
|
||||||
.read()
|
.read()
|
||||||
@ -234,18 +226,6 @@ impl crate::agent::SkillProvider for SkillRuntime {
|
|||||||
fn system_index_prompt(&self) -> Option<String> {
|
fn system_index_prompt(&self) -> Option<String> {
|
||||||
SkillRuntime::system_index_prompt(self)
|
SkillRuntime::system_index_prompt(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skill_tool_definition(&self) -> Option<Tool> {
|
|
||||||
SkillRuntime::skill_tool_definition(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn activation_payload(&self, name: &str) -> Result<String, String> {
|
|
||||||
SkillRuntime::activation_payload(self, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn activation_event_payload(&self, name: &str) -> Result<serde_json::Value, String> {
|
|
||||||
SkillRuntime::activation_event_payload(self, name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SkillSource {
|
impl SkillSource {
|
||||||
@ -362,30 +342,6 @@ impl SkillCatalog {
|
|||||||
self.catalog_event_payload()
|
self.catalog_event_payload()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn skill_tool_definition(&self) -> Option<Tool> {
|
|
||||||
if self.skills.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Tool {
|
|
||||||
tool_type: "function".to_string(),
|
|
||||||
function: ToolFunction {
|
|
||||||
name: "skill_activate".to_string(),
|
|
||||||
description: "Load detailed instructions for a named skill discovered from SKILL.md files. Use when a task matches a listed skill description.".to_string(),
|
|
||||||
parameters: json!({
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Skill name from the available skills list"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["name"]
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn activation_payload(&self, name: &str) -> Result<String, String> {
|
pub fn activation_payload(&self, name: &str) -> Result<String, String> {
|
||||||
let skill = self
|
let skill = self
|
||||||
.find_skill(name)
|
.find_skill(name)
|
||||||
@ -697,31 +653,6 @@ mod tests {
|
|||||||
assert!(err.contains("description"));
|
assert!(err.contains("description"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_skill_tool_definition_exists_when_skills_present() {
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
|
||||||
let root = dir.path().join(".picobot").join("skills").join("demo");
|
|
||||||
fs::create_dir_all(&root).unwrap();
|
|
||||||
fs::write(
|
|
||||||
root.join("SKILL.md"),
|
|
||||||
"---\ndescription: demo skill\n---\nDo demo",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let skills = load_skills_from_root(
|
|
||||||
&dir.path().join(".picobot").join("skills"),
|
|
||||||
SkillSource::Project,
|
|
||||||
);
|
|
||||||
let catalog = SkillCatalog {
|
|
||||||
skills,
|
|
||||||
max_index_chars: 4000,
|
|
||||||
max_listed_skills: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
let tool = catalog.skill_tool_definition().unwrap();
|
|
||||||
assert_eq!(tool.function.name, "skill_activate");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_activation_payload_contains_body() {
|
fn test_activation_payload_contains_body() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@ -9,6 +9,7 @@ pub mod memory_search;
|
|||||||
pub mod registry;
|
pub mod registry;
|
||||||
pub mod scheduler_manage;
|
pub mod scheduler_manage;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
pub mod skill_activate;
|
||||||
pub mod skill_manage;
|
pub mod skill_manage;
|
||||||
pub mod time;
|
pub mod time;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
@ -25,6 +26,7 @@ pub use memory_search::MemorySearchTool;
|
|||||||
pub use registry::ToolRegistry;
|
pub use registry::ToolRegistry;
|
||||||
pub use scheduler_manage::SchedulerManageTool;
|
pub use scheduler_manage::SchedulerManageTool;
|
||||||
pub use schema::{CleaningStrategy, SchemaCleanr};
|
pub use schema::{CleaningStrategy, SchemaCleanr};
|
||||||
|
pub use skill_activate::SkillActivateTool;
|
||||||
pub use skill_manage::{SkillListTool, SkillManageTool};
|
pub use skill_manage::{SkillListTool, SkillManageTool};
|
||||||
pub use time::TimeTool;
|
pub use time::TimeTool;
|
||||||
pub use traits::{Tool, ToolContext, ToolResult};
|
pub use traits::{Tool, ToolContext, ToolResult};
|
||||||
|
|||||||
151
src/tools/skill_activate.rs
Normal file
151
src/tools/skill_activate.rs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::skills::SkillRuntime;
|
||||||
|
use crate::storage::SkillEventRepository;
|
||||||
|
use crate::tools::traits::{Tool, ToolContext, ToolResult};
|
||||||
|
|
||||||
|
pub struct SkillActivateTool {
|
||||||
|
skills: Arc<SkillRuntime>,
|
||||||
|
events: Arc<dyn SkillEventRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SkillActivateTool {
|
||||||
|
pub fn new(skills: Arc<SkillRuntime>, events: Arc<dyn SkillEventRepository>) -> Self {
|
||||||
|
Self { skills, events }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_event(
|
||||||
|
&self,
|
||||||
|
context: &ToolContext,
|
||||||
|
event_type: &str,
|
||||||
|
skill_name: Option<&str>,
|
||||||
|
payload: &serde_json::Value,
|
||||||
|
) {
|
||||||
|
if let Err(err) = self.events.append_skill_event(
|
||||||
|
context.session_id.as_deref(),
|
||||||
|
event_type,
|
||||||
|
skill_name,
|
||||||
|
payload,
|
||||||
|
) {
|
||||||
|
tracing::warn!(error = %err, event_type, skill_name, "Failed to record skill activation event");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Tool for SkillActivateTool {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"skill_activate"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"Load detailed instructions for a named skill discovered from SKILL.md files. Use when a task matches a listed skill description."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parameters_schema(&self) -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Skill name from the available skills list"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
||||||
|
self.execute_with_context(&ToolContext::default(), args)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_with_context(
|
||||||
|
&self,
|
||||||
|
context: &ToolContext,
|
||||||
|
args: serde_json::Value,
|
||||||
|
) -> anyhow::Result<ToolResult> {
|
||||||
|
let skill_name = match args.get("name").and_then(|value| value.as_str()) {
|
||||||
|
Some(name) if !name.trim().is_empty() => name,
|
||||||
|
_ => {
|
||||||
|
self.record_event(
|
||||||
|
context,
|
||||||
|
"activation_failed",
|
||||||
|
None,
|
||||||
|
&json!({
|
||||||
|
"reason": "missing_name",
|
||||||
|
"arguments": args,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return Ok(error_result("Missing required parameter: name"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.skills.activation_payload(skill_name) {
|
||||||
|
Ok(output) => {
|
||||||
|
if let Ok(payload) = self.skills.activation_event_payload(skill_name) {
|
||||||
|
self.record_event(context, "activated", Some(skill_name), &payload);
|
||||||
|
}
|
||||||
|
Ok(ToolResult {
|
||||||
|
success: true,
|
||||||
|
output,
|
||||||
|
error: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.record_event(
|
||||||
|
context,
|
||||||
|
"activation_failed",
|
||||||
|
Some(skill_name),
|
||||||
|
&json!({
|
||||||
|
"reason": err,
|
||||||
|
"arguments": args,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
Ok(error_result(&err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_result(message: &str) -> ToolResult {
|
||||||
|
ToolResult {
|
||||||
|
success: false,
|
||||||
|
output: String::new(),
|
||||||
|
error: Some(message.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::storage::SessionStore;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_skill_activate_records_failed_activation_event() {
|
||||||
|
let skills = Arc::new(SkillRuntime::default());
|
||||||
|
let store = Arc::new(SessionStore::in_memory().unwrap());
|
||||||
|
store.ensure_channel_session("feishu", "chat-1").unwrap();
|
||||||
|
let tool = SkillActivateTool::new(skills, store.clone());
|
||||||
|
let context = ToolContext {
|
||||||
|
session_id: Some("feishu:chat-1".to_string()),
|
||||||
|
..ToolContext::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = tool
|
||||||
|
.execute_with_context(&context, json!({ "name": "demo" }))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!result.success);
|
||||||
|
assert!(result.error.unwrap().contains("not found"));
|
||||||
|
|
||||||
|
let events = store.list_skill_events(Some("feishu:chat-1")).unwrap();
|
||||||
|
assert_eq!(events.len(), 1);
|
||||||
|
assert_eq!(events[0].event_type, "activation_failed");
|
||||||
|
assert_eq!(events[0].skill_name.as_deref(), Some("demo"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user