Skip to main content

bamboo_agent/server/handlers/
skill.rs

1use crate::agent::skill::{SkillDefinition, SkillFilter};
2use crate::agent::tools::BuiltinToolExecutor;
3use actix_web::{web, HttpResponse};
4use log::{debug, info};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::server::app_state::AppState;
9use crate::server::error::AppError;
10
11/// Configure skill routes
12pub fn config(cfg: &mut web::ServiceConfig) {
13    cfg.route("/skills", web::get().to(list_skills))
14        .route("/skills/{id}", web::get().to(get_skill))
15        .route(
16            "/skills/available-tools",
17            web::get().to(get_available_tools),
18        )
19        .route("/skills/filtered-tools", web::get().to(get_filtered_tools))
20        .route(
21            "/skills/available-workflows",
22            web::get().to(get_available_workflows),
23        );
24}
25
26#[derive(Serialize)]
27struct SkillListResponse {
28    skills: Vec<SkillDefinition>,
29    total: usize,
30}
31
32#[derive(Deserialize)]
33pub struct ListSkillsQuery {
34    category: Option<String>,
35    search: Option<String>,
36    refresh: Option<bool>,
37}
38
39#[derive(Serialize)]
40struct AvailableToolsResponse {
41    tools: Vec<String>,
42}
43
44#[derive(Serialize)]
45struct FilteredToolsResponse {
46    tools: Vec<OpenAiTool>,
47}
48
49#[derive(Serialize)]
50struct OpenAiTool {
51    #[serde(rename = "type")]
52    tool_type: String,
53    function: OpenAiFunction,
54}
55
56#[derive(Serialize)]
57struct OpenAiFunction {
58    name: String,
59    description: String,
60    parameters: Value,
61}
62
63#[derive(Serialize)]
64struct AvailableWorkflowsResponse {
65    workflows: Vec<String>,
66}
67
68/// GET /skills - List all skills
69pub async fn list_skills(
70    state: web::Data<AppState>,
71    query: web::Query<ListSkillsQuery>,
72) -> Result<HttpResponse, AppError> {
73    let mut filter = SkillFilter::new();
74    if let Some(category) = query.category.clone() {
75        filter = filter.with_category(category);
76    }
77    if let Some(search) = query.search.clone() {
78        filter = filter.with_search(search);
79    }
80
81    let refresh = query.refresh.unwrap_or(false);
82    let skills = state
83        .skill_manager
84        .as_ref()
85        .store()
86        .list_skills(Some(filter), refresh)
87        .await;
88
89    Ok(HttpResponse::Ok().json(SkillListResponse {
90        total: skills.len(),
91        skills,
92    }))
93}
94
95/// GET /skills/{id} - Get skill detail
96pub async fn get_skill(
97    state: web::Data<AppState>,
98    path: web::Path<String>,
99) -> Result<HttpResponse, AppError> {
100    let id = path.into_inner();
101    let skill = state
102        .skill_manager
103        .as_ref()
104        .store()
105        .get_skill(&id)
106        .await
107        .map_err(|_| AppError::NotFound(format!("Skill {} not found", id)))?;
108
109    Ok(HttpResponse::Ok().json(skill))
110}
111
112/// GET /skills/available-tools - Get available built-in tools
113pub async fn get_available_tools(_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
114    let tool_names: Vec<String> = BuiltinToolExecutor::tool_schemas()
115        .into_iter()
116        .map(|tool| tool.function.name)
117        .collect();
118
119    Ok(HttpResponse::Ok().json(AvailableToolsResponse { tools: tool_names }))
120}
121
122#[derive(Deserialize)]
123pub struct FilteredToolsQuery {
124    chat_id: Option<String>,
125}
126
127/// GET /skills/filtered-tools - Get tools filtered by enabled skills
128pub async fn get_filtered_tools(
129    state: web::Data<AppState>,
130    query: web::Query<FilteredToolsQuery>,
131) -> Result<HttpResponse, AppError> {
132    let allowed_tools = state
133        .skill_manager
134        .as_ref()
135        .get_allowed_tools(query.chat_id.as_deref())
136        .await;
137    debug!("Skill filtered tools allowed list: {:?}", allowed_tools);
138
139    let all_tools = BuiltinToolExecutor::tool_schemas();
140    let all_tool_names: Vec<String> = all_tools
141        .iter()
142        .map(|tool| tool.function.name.clone())
143        .collect();
144    debug!("Built-in tools discovered: {:?}", all_tool_names);
145
146    let filtered = if allowed_tools.is_empty() {
147        info!("No enabled skills; returning all {} tools", all_tools.len());
148        all_tools
149    } else {
150        let filtered: Vec<_> = all_tools
151            .into_iter()
152            .filter(|tool| {
153                allowed_tools
154                    .iter()
155                    .any(|allowed| allowed == &tool.function.name)
156            })
157            .collect();
158        info!(
159            "Filtered tools: allowed={}, matched={}",
160            allowed_tools.len(),
161            filtered.len()
162        );
163        filtered
164    };
165
166    let tools = filtered
167        .into_iter()
168        .map(|tool| OpenAiTool {
169            tool_type: "function".to_string(),
170            function: OpenAiFunction {
171                name: tool.function.name,
172                description: tool.function.description,
173                parameters: tool.function.parameters,
174            },
175        })
176        .collect();
177
178    Ok(HttpResponse::Ok().json(FilteredToolsResponse { tools }))
179}
180
181/// GET /skills/available-workflows - Get available workflows
182pub async fn get_available_workflows(state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
183    let workflows = crate::server::services::skill_service::list_workflows(&state.app_data_dir)
184        .await
185        .map_err(|e| AppError::InternalError(anyhow::anyhow!("Failed to list workflows: {}", e)))?;
186
187    Ok(HttpResponse::Ok().json(AvailableWorkflowsResponse { workflows }))
188}