use crate::client::KibanaClient;
use crate::etl::Extractor;
use crate::{Error, Result, ResultContext};
use serde_json::Value;
use tokio::task::JoinSet;
pub struct ToolsExtractor {
client: KibanaClient,
manifest: Option<super::ToolsManifest>,
}
impl ToolsExtractor {
pub fn new(client: KibanaClient, manifest: Option<super::ToolsManifest>) -> Self {
Self { client, manifest }
}
pub async fn search_tools(&self, _query: Option<&str>) -> Result<Vec<Value>> {
let path = "api/agent_builder/tools";
tracing::debug!(
"Fetching tools from {} in space '{}'",
path,
self.client.space_id()
);
let response = self
.client
.get(path)
.await
.context("Failed to fetch tools")?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(Error::api_response(status, body));
}
let search_result: Value = response
.json()
.await
.context("Failed to parse tools response")?;
let tools: Vec<Value> = search_result
.get("results")
.and_then(|v| v.as_array())
.map(|arr| arr.to_vec())
.unwrap_or_default();
tracing::info!("Found {} tool(s) via search", tools.len());
Ok(tools)
}
async fn fetch_manifest_tools(&self, manifest: &super::ToolsManifest) -> Result<Vec<Value>> {
let mut tools = Vec::new();
let mut set = JoinSet::new();
for tool_id in &manifest.tools {
let client = self.client.clone();
let tool_id = tool_id.clone();
set.spawn(async move {
let path = format!("api/agent_builder/tools/{}", tool_id);
tracing::debug!(
"Fetching tool '{}' from space '{}'",
tool_id,
client.space_id()
);
let response = client
.get(&path)
.await
.with_context(|| format!("Failed to fetch tool '{}'", tool_id))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(Error::api_response(status, body));
}
let tool: Value = response
.json()
.await
.with_context(|| format!("Failed to parse tool '{}' response", tool_id))?;
tracing::debug!("Fetched tool: {}", tool_id);
Ok::<Value, Error>(tool)
});
}
while let Some(res) = set.join_next().await {
match res {
Ok(Ok(tool)) => tools.push(tool),
Ok(Err(e)) => tracing::warn!("{}", e),
Err(e) => tracing::error!("Task panicked: {}", e),
}
}
tracing::info!("Fetched {} tool(s) from manifest", tools.len());
Ok(tools)
}
}
impl Extractor for ToolsExtractor {
type Item = Value;
async fn extract(&self) -> Result<Vec<Self::Item>> {
let tools = if let Some(manifest) = &self.manifest {
self.fetch_manifest_tools(manifest).await?
} else {
tracing::warn!("No manifest provided - use search API to discover tools");
Vec::new()
};
tracing::info!(
"Extracted {} tool(s){}",
tools.len(),
if self.manifest.is_some() {
" (from manifest)"
} else {
""
}
);
Ok(tools)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::{Auth, KibanaClient};
use url::Url;
#[test]
fn test_extractor_creation() {
let url = Url::parse("http://localhost:5601").unwrap();
let client = KibanaClient::new(url, Auth::None).unwrap();
let space_client = client.space("default").unwrap();
let _extractor = ToolsExtractor::new(space_client, None);
}
}