kibana_sync/kibana/tools/
extractor.rs1use crate::client::KibanaClient;
6use crate::etl::Extractor;
7
8use crate::{Error, Result, ResultContext};
9use serde_json::Value;
10use tokio::task::JoinSet;
11
12pub struct ToolsExtractor {
39 client: KibanaClient,
40 manifest: Option<super::ToolsManifest>,
41}
42
43impl ToolsExtractor {
44 pub fn new(client: KibanaClient, manifest: Option<super::ToolsManifest>) -> Self {
50 Self { client, manifest }
51 }
52
53 pub async fn search_tools(&self, _query: Option<&str>) -> Result<Vec<Value>> {
64 let path = "api/agent_builder/tools";
65
66 tracing::debug!(
67 "Fetching tools from {} in space '{}'",
68 path,
69 self.client.space_id()
70 );
71
72 let response = self
73 .client
74 .get(path)
75 .await
76 .context("Failed to fetch tools")?;
77
78 if !response.status().is_success() {
79 let status = response.status();
80 let body = response.text().await.unwrap_or_default();
81 return Err(Error::api_response(status, body));
82 }
83
84 let search_result: Value = response
85 .json()
86 .await
87 .context("Failed to parse tools response")?;
88
89 let tools: Vec<Value> = search_result
91 .get("results")
92 .and_then(|v| v.as_array())
93 .map(|arr| arr.to_vec())
94 .unwrap_or_default();
95
96 tracing::info!("Found {} tool(s) via search", tools.len());
97
98 Ok(tools)
99 }
100
101 async fn fetch_manifest_tools(&self, manifest: &super::ToolsManifest) -> Result<Vec<Value>> {
103 let mut tools = Vec::new();
104 let mut set = JoinSet::new();
105
106 for tool_id in &manifest.tools {
107 let client = self.client.clone();
108 let tool_id = tool_id.clone();
109
110 set.spawn(async move {
111 let path = format!("api/agent_builder/tools/{}", tool_id);
112 tracing::debug!(
113 "Fetching tool '{}' from space '{}'",
114 tool_id,
115 client.space_id()
116 );
117
118 let response = client
119 .get(&path)
120 .await
121 .with_context(|| format!("Failed to fetch tool '{}'", tool_id))?;
122
123 if !response.status().is_success() {
124 let status = response.status();
125 let body = response.text().await.unwrap_or_default();
126 return Err(Error::api_response(status, body));
127 }
128
129 let tool: Value = response
130 .json()
131 .await
132 .with_context(|| format!("Failed to parse tool '{}' response", tool_id))?;
133
134 tracing::debug!("Fetched tool: {}", tool_id);
135 Ok::<Value, Error>(tool)
136 });
137 }
138
139 while let Some(res) = set.join_next().await {
140 match res {
141 Ok(Ok(tool)) => tools.push(tool),
142 Ok(Err(e)) => tracing::warn!("{}", e),
143 Err(e) => tracing::error!("Task panicked: {}", e),
144 }
145 }
146
147 tracing::info!("Fetched {} tool(s) from manifest", tools.len());
148
149 Ok(tools)
150 }
151}
152
153impl Extractor for ToolsExtractor {
154 type Item = Value;
155
156 async fn extract(&self) -> Result<Vec<Self::Item>> {
157 let tools = if let Some(manifest) = &self.manifest {
158 self.fetch_manifest_tools(manifest).await?
160 } else {
161 tracing::warn!("No manifest provided - use search API to discover tools");
164 Vec::new()
165 };
166
167 tracing::info!(
168 "Extracted {} tool(s){}",
169 tools.len(),
170 if self.manifest.is_some() {
171 " (from manifest)"
172 } else {
173 ""
174 }
175 );
176
177 Ok(tools)
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::client::{Auth, KibanaClient};
185 use url::Url;
186
187 #[test]
188 fn test_extractor_creation() {
189 let url = Url::parse("http://localhost:5601").unwrap();
190 let client = KibanaClient::new(url, Auth::None).unwrap();
191 let space_client = client.space("default").unwrap();
192 let _extractor = ToolsExtractor::new(space_client, None);
193 }
194}