fix: Lower KB search thresholds and add Cloudflare AI embedding support
All checks were successful
BotServer CI / build (push) Successful in 10m35s
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:
parent
8500949fcd
commit
859db6b8a0
3 changed files with 99 additions and 4 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue