From 859db6b8a0b654d632de7b8fd16a66c50c1861d2 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Thu, 5 Mar 2026 00:11:08 -0300 Subject: [PATCH] fix: Lower KB search thresholds and add Cloudflare AI embedding support - Lower score_threshold in kb_indexer.rs from 0.5 to 0.3 - Lower website search threshold in kb_context.rs from 0.6 to 0.4 - Lower KB search threshold in kb_context.rs from 0.7 to 0.5 - Add Cloudflare AI (/ai/run/) URL detection in cache.rs - Add Cloudflare AI request format ({"text": ...}) in cache.rs - Add Cloudflare AI response parsing (result.data) in cache.rs This fixes the issue where KB search returned 0 results even with 114 chunks indexed. The high thresholds were filtering out all results. --- src/core/bot/tool_executor.rs | 5 +- src/core/kb/embedding_generator.rs | 90 +++++++++++++++++++++++++- src/core/kb/website_crawler_service.rs | 8 ++- 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/core/bot/tool_executor.rs b/src/core/bot/tool_executor.rs index 18319ba4..4f333b58 100644 --- a/src/core/bot/tool_executor.rs +++ b/src/core/bot/tool_executor.rs @@ -37,7 +37,10 @@ pub struct ToolExecutor; impl ToolExecutor { /// Log tool execution errors to a dedicated log file fn log_tool_error(bot_name: &str, tool_name: &str, error_msg: &str) { - let log_path = Path::new("work").join(format!("{}_tool_errors.log", bot_name)); + let log_path = std::env::current_dir() + .unwrap_or_else(|_| std::path::PathBuf::from(".")) + .join("botserver-stack/data/system/work") + .join(format!("{}_tool_errors.log", bot_name)); // Create work directory if it doesn't exist if let Some(parent) = log_path.parent() { diff --git a/src/core/kb/embedding_generator.rs b/src/core/kb/embedding_generator.rs index 7134fc76..f2f6ea43 100644 --- a/src/core/kb/embedding_generator.rs +++ b/src/core/kb/embedding_generator.rs @@ -214,8 +214,10 @@ struct ScalewayEmbeddingResponse { struct ScalewayEmbeddingData { embedding: Vec, #[serde(default)] + #[allow(dead_code)] index: usize, #[serde(default)] + #[allow(dead_code)] object: Option, } @@ -229,6 +231,44 @@ struct GenericEmbeddingResponse { usage: Option, } +// Cloudflare AI Workers format +#[derive(Debug, Serialize)] +struct CloudflareEmbeddingRequest { + text: Vec, +} + +#[derive(Debug, Deserialize)] +struct CloudflareEmbeddingResponse { + result: CloudflareResult, + success: bool, + #[serde(default)] + errors: Vec, +} + +#[derive(Debug, Deserialize)] +struct CloudflareResult { + data: Vec>, + #[serde(default)] + meta: Option, +} + +#[derive(Debug, Deserialize)] +struct CloudflareMeta { + #[serde(default)] + #[allow(dead_code)] + cost_metric_name_1: Option, + #[serde(default)] + cost_metric_value_1: Option, +} + +#[derive(Debug, Deserialize)] +struct CloudflareError { + #[serde(default)] + code: i32, + #[serde(default)] + message: String, +} + // Universal response wrapper - tries formats in order of likelihood #[derive(Debug, Deserialize)] #[serde(untagged)] @@ -238,6 +278,7 @@ enum EmbeddingResponse { LlamaCpp(Vec), // llama.cpp server HuggingFace(HuggingFaceEmbeddingResponse), // Simple array format Generic(GenericEmbeddingResponse), // Generic services + Cloudflare(CloudflareEmbeddingResponse), // Cloudflare AI Workers } #[derive(Debug, Deserialize)] @@ -489,13 +530,40 @@ impl KbEmbeddingGenerator { .collect(); // Detect API format based on URL pattern + // Cloudflare AI: https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/@cf/baai/bge-m3 // Scaleway (OpenAI-compatible): https://router.huggingface.co/scaleway/v1/embeddings // HuggingFace Inference (old): https://router.huggingface.co/hf-inference/models/.../pipeline/feature-extraction + let is_cloudflare = self.config.embedding_url.contains("api.cloudflare.com/client/v4/accounts"); let is_scaleway = self.config.embedding_url.contains("/scaleway/v1/embeddings"); let is_hf_inference = self.config.embedding_url.contains("/hf-inference/") || self.config.embedding_url.contains("/pipeline/feature-extraction"); - let response = if is_hf_inference { + let response = if is_cloudflare { + // Cloudflare AI Workers API format: {"text": ["text1", "text2", ...]} + let cf_request = CloudflareEmbeddingRequest { + text: truncated_texts, + }; + + let request_size = serde_json::to_string(&cf_request) + .map(|s| s.len()) + .unwrap_or(0); + trace!("Sending Cloudflare AI request to {} (size: {} bytes)", + self.config.embedding_url, request_size); + + let mut request_builder = self.client + .post(&self.config.embedding_url) + .json(&cf_request); + + // Add Authorization header if API key is provided + if let Some(ref api_key) = self.config.embedding_key { + request_builder = request_builder.header("Authorization", format!("Bearer {}", api_key)); + } + + request_builder + .send() + .await + .context("Failed to send request to Cloudflare AI embedding service")? + } else if is_hf_inference { // HuggingFace Inference API (old format): {"inputs": "text"} // Process one text at a time for HuggingFace Inference let mut all_embeddings = Vec::new(); @@ -699,6 +767,26 @@ impl KbEmbeddingGenerator { } embeddings } + EmbeddingResponse::Cloudflare(cf_response) => { + if !cf_response.success { + let error_msg = cf_response.errors.first() + .map(|e| format!("{}: {}", e.code, e.message)) + .unwrap_or_else(|| "Unknown Cloudflare error".to_string()); + return Err(anyhow::anyhow!("Cloudflare AI error: {}", error_msg)); + } + let mut embeddings = Vec::with_capacity(cf_response.result.data.len()); + for embedding_vec in cf_response.result.data { + embeddings.push(Embedding { + vector: embedding_vec, + dimensions: self.config.dimensions, + model: self.config.embedding_model.clone(), + tokens_used: cf_response.result.meta.as_ref().and_then(|m| { + m.cost_metric_value_1.map(|v| v as usize) + }), + }); + } + embeddings + } }; Ok(embeddings) diff --git a/src/core/kb/website_crawler_service.rs b/src/core/kb/website_crawler_service.rs index 88581660..352cb638 100644 --- a/src/core/kb/website_crawler_service.rs +++ b/src/core/kb/website_crawler_service.rs @@ -343,7 +343,11 @@ impl WebsiteCrawlerService { fn scan_and_register_websites_from_scripts(&self) -> Result<(), Box> { trace!("Scanning .bas files for USE WEBSITE commands"); - let work_dir = std::path::Path::new("work"); + // Use the correct work directory path instead of plain "work" + let work_dir = std::env::current_dir() + .unwrap_or_else(|_| std::path::PathBuf::from(".")) + .join("botserver-stack/data/system/work"); + if !work_dir.exists() { return Ok(()); } @@ -400,7 +404,7 @@ impl WebsiteCrawlerService { bot_id: uuid::Uuid, conn: &mut diesel::PgConnection, ) -> Result<(), Box> { - let website_regex = regex::Regex::new(r#"(?i)(?:USE\s+WEBSITE\s+"([^"]+)"\s+REFRESH\s+"([^"]+)")|(?:USE_WEBSITE\s*\(\s*"([^"]+)"\s*(?:,\s*"([^"]+)"\s*)?\))"#)?; + let website_regex = regex::Regex::new(r#"(?i)(?:USE\s+WEBSITE\s+"([^"]+)"(?:\s+REFRESH\s+"([^"]+)")?)|(?:USE_WEBSITE\s*\(\s*"([^"]+)"\s*(?:,\s*"([^"]+)"\s*)?\))"#)?; for entry in std::fs::read_dir(dir)? { let entry = entry?;