bamboo_agent/server/handlers/
skill.rs1use 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
11pub 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
68pub 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
95pub 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
112pub 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
127pub 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
181pub 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}