fix: Lower KB search thresholds and add Cloudflare AI embedding support
All checks were successful
BotServer CI / build (push) Successful in 10m35s

- 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.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-03-05 00:11:08 -03:00
parent 8500949fcd
commit 859db6b8a0
3 changed files with 99 additions and 4 deletions

View file

@ -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() {

View file

@ -214,8 +214,10 @@ struct ScalewayEmbeddingResponse {
struct ScalewayEmbeddingData {
embedding: Vec<f32>,
#[serde(default)]
#[allow(dead_code)]
index: usize,
#[serde(default)]
#[allow(dead_code)]
object: Option<String>,
}
@ -229,6 +231,44 @@ struct GenericEmbeddingResponse {
usage: Option<EmbeddingUsage>,
}
// Cloudflare AI Workers format
#[derive(Debug, Serialize)]
struct CloudflareEmbeddingRequest {
text: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct CloudflareEmbeddingResponse {
result: CloudflareResult,
success: bool,
#[serde(default)]
errors: Vec<CloudflareError>,
}
#[derive(Debug, Deserialize)]
struct CloudflareResult {
data: Vec<Vec<f32>>,
#[serde(default)]
meta: Option<CloudflareMeta>,
}
#[derive(Debug, Deserialize)]
struct CloudflareMeta {
#[serde(default)]
#[allow(dead_code)]
cost_metric_name_1: Option<String>,
#[serde(default)]
cost_metric_value_1: Option<f64>,
}
#[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<LlamaCppEmbeddingItem>), // 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)

View file

@ -343,7 +343,11 @@ impl WebsiteCrawlerService {
fn scan_and_register_websites_from_scripts(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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?;