Skip to main content

kibana_sync/kibana/tools/
extractor.rs

1//! Tools API extractor
2//!
3//! Extracts tool definitions from Kibana via GET /api/agent_builder/tools
4
5use crate::client::KibanaClient;
6use crate::etl::Extractor;
7
8use crate::{Error, Result, ResultContext};
9use serde_json::Value;
10use tokio::task::JoinSet;
11
12/// Extractor for Kibana tools
13///
14/// Fetches tools by ID from the manifest. If no manifest is provided,
15/// you should use the search API to discover tools first.
16///
17/// # Example
18/// ```no_run
19/// use kibana_sync::kibana::tools::{ToolsExtractor, ToolsManifest};
20/// use kibana_sync::client::{Auth, KibanaClient};
21/// use kibana_sync::etl::Extractor;
22/// use url::Url;
23///
24/// # async fn example() -> kibana_sync::Result<()> {
25/// let url = Url::parse("http://localhost:5601")?;
26/// let client = KibanaClient::new(url, Auth::None)?;
27/// let space_client = client.space("default")?;
28/// let manifest = ToolsManifest::with_tools(vec![
29///     "platform.core.search".to_string(),
30///     "platform.core.get_document_by_id".to_string()
31/// ]);
32///
33/// let extractor = ToolsExtractor::new(space_client, Some(manifest));
34/// let tools = extractor.extract().await?;
35/// # Ok(())
36/// # }
37/// ```
38pub struct ToolsExtractor {
39    client: KibanaClient,
40    manifest: Option<super::ToolsManifest>,
41}
42
43impl ToolsExtractor {
44    /// Create a new tools extractor
45    ///
46    /// # Arguments
47    /// * `client` - Space-scoped Kibana client
48    /// * `manifest` - Manifest containing tool IDs to extract
49    pub fn new(client: KibanaClient, manifest: Option<super::ToolsManifest>) -> Self {
50        Self { client, manifest }
51    }
52
53    /// Search for tools via the Tools API
54    ///
55    /// Uses GET /api/agent_builder/tools to fetch all tools.
56    /// This is useful for discovering tools before adding them to the manifest.
57    ///
58    /// # Arguments
59    /// * `_query` - Reserved for future use (currently unused)
60    ///
61    /// # Returns
62    /// Vector of tool JSON objects from the search results
63    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        // Extract tools from results array
90        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    /// Fetch specific tools by ID from manifest
102    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            // Fetch only tools from manifest by ID
159            self.fetch_manifest_tools(manifest).await?
160        } else {
161            // No manifest provided - return empty list
162            // Use search API separately to discover tools
163            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}