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 {
|
impl ToolExecutor {
|
||||||
/// Log tool execution errors to a dedicated log file
|
/// Log tool execution errors to a dedicated log file
|
||||||
fn log_tool_error(bot_name: &str, tool_name: &str, error_msg: &str) {
|
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
|
// Create work directory if it doesn't exist
|
||||||
if let Some(parent) = log_path.parent() {
|
if let Some(parent) = log_path.parent() {
|
||||||
|
|
|
||||||
|
|
@ -214,8 +214,10 @@ struct ScalewayEmbeddingResponse {
|
||||||
struct ScalewayEmbeddingData {
|
struct ScalewayEmbeddingData {
|
||||||
embedding: Vec<f32>,
|
embedding: Vec<f32>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[allow(dead_code)]
|
||||||
index: usize,
|
index: usize,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[allow(dead_code)]
|
||||||
object: Option<String>,
|
object: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,6 +231,44 @@ struct GenericEmbeddingResponse {
|
||||||
usage: Option<EmbeddingUsage>,
|
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
|
// Universal response wrapper - tries formats in order of likelihood
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
|
@ -238,6 +278,7 @@ enum EmbeddingResponse {
|
||||||
LlamaCpp(Vec<LlamaCppEmbeddingItem>), // llama.cpp server
|
LlamaCpp(Vec<LlamaCppEmbeddingItem>), // llama.cpp server
|
||||||
HuggingFace(HuggingFaceEmbeddingResponse), // Simple array format
|
HuggingFace(HuggingFaceEmbeddingResponse), // Simple array format
|
||||||
Generic(GenericEmbeddingResponse), // Generic services
|
Generic(GenericEmbeddingResponse), // Generic services
|
||||||
|
Cloudflare(CloudflareEmbeddingResponse), // Cloudflare AI Workers
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -489,13 +530,40 @@ impl KbEmbeddingGenerator {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Detect API format based on URL pattern
|
// 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
|
// Scaleway (OpenAI-compatible): https://router.huggingface.co/scaleway/v1/embeddings
|
||||||
// HuggingFace Inference (old): https://router.huggingface.co/hf-inference/models/.../pipeline/feature-extraction
|
// 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_scaleway = self.config.embedding_url.contains("/scaleway/v1/embeddings");
|
||||||
let is_hf_inference = self.config.embedding_url.contains("/hf-inference/") ||
|
let is_hf_inference = self.config.embedding_url.contains("/hf-inference/") ||
|
||||||
self.config.embedding_url.contains("/pipeline/feature-extraction");
|
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"}
|
// HuggingFace Inference API (old format): {"inputs": "text"}
|
||||||
// Process one text at a time for HuggingFace Inference
|
// Process one text at a time for HuggingFace Inference
|
||||||
let mut all_embeddings = Vec::new();
|
let mut all_embeddings = Vec::new();
|
||||||
|
|
@ -699,6 +767,26 @@ impl KbEmbeddingGenerator {
|
||||||
}
|
}
|
||||||
embeddings
|
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)
|
Ok(embeddings)
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,11 @@ impl WebsiteCrawlerService {
|
||||||
fn scan_and_register_websites_from_scripts(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
fn scan_and_register_websites_from_scripts(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
trace!("Scanning .bas files for USE WEBSITE commands");
|
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() {
|
if !work_dir.exists() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -400,7 +404,7 @@ impl WebsiteCrawlerService {
|
||||||
bot_id: uuid::Uuid,
|
bot_id: uuid::Uuid,
|
||||||
conn: &mut diesel::PgConnection,
|
conn: &mut diesel::PgConnection,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> 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)? {
|
for entry in std::fs::read_dir(dir)? {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue