Add SCAN BARCODE keyword and BotModelsClient.scan_barcode

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-14 11:43:34 -03:00
parent ee9341163f
commit e4524d0584
2 changed files with 137 additions and 0 deletions

View file

@ -1,5 +1,6 @@
use crate::bot::get_default_bot;
use crate::core::shared::schema::products;
use crate::multimodal::BotModelsClient;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use crate::shared::utils;
@ -9,6 +10,7 @@ use log::{error, trace};
use rhai::{Dynamic, Engine};
use serde_json::{json, Value};
use std::sync::Arc;
use std::time::Duration;
#[derive(QueryableByName)]
struct JsonRow {
@ -186,6 +188,82 @@ pub fn products_keyword(state: &AppState, _user: UserSession, engine: &mut Engin
}
}
});
let state_barcode = state.clone();
let user_barcode = _user.clone();
engine
.register_custom_syntax(["SCAN", "BARCODE", "$expr$"], false, {
move |context, inputs| {
let image_path = context.eval_expression_tree(&inputs[0])?.to_string();
trace!("SCAN BARCODE: {}", image_path);
let state_clone = state_barcode.clone();
let bot_id = user_barcode.bot_id;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build();
let send_err = if let Ok(rt) = rt {
let result = rt.block_on(async move {
scan_barcode(&state_clone, &bot_id, &image_path).await
});
tx.send(result).err()
} else {
tx.send(Err("Failed to build runtime".into())).err()
};
if send_err.is_some() {
error!("Failed to send SCAN BARCODE result");
}
});
match rx.recv_timeout(Duration::from_secs(30)) {
Ok(Ok(result)) => Ok(utils::json_value_to_dynamic(&result)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.to_string().into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"SCAN BARCODE timed out".into(),
rhai::Position::NONE,
))),
}
}
})
.expect("valid syntax");
engine.register_fn("SCAN_BARCODE", {
let state_clone = state.clone();
let bot_id = _user.bot_id;
move |image_path: String| -> Dynamic {
let state_for_task = state_clone.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build();
if let Ok(rt) = rt {
let result = rt.block_on(async move {
scan_barcode(&state_for_task, &bot_id, &image_path).await
});
let _ = tx.send(result);
}
});
match rx.recv_timeout(Duration::from_secs(30)) {
Ok(Ok(result)) => utils::json_value_to_dynamic(&result),
_ => Dynamic::UNIT,
}
}
});
}
fn get_all_products(conn: &mut diesel::PgConnection) -> Result<Value, String> {
@ -237,6 +315,21 @@ fn get_product_by_id(conn: &mut diesel::PgConnection, id: i64) -> Result<Option<
.and_then(|row| serde_json::from_str(&row.row_data).ok()))
}
async fn scan_barcode(
state: &AppState,
bot_id: &uuid::Uuid,
image_path: &str,
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
let client = BotModelsClient::from_state(state, bot_id);
if !client.is_enabled() {
return Err("BotModels not enabled".into());
}
let result = client.scan_barcode(image_path).await?;
Ok(serde_json::from_str(&result).unwrap_or(json!({"data": result})))
}
fn search_products(conn: &mut diesel::PgConnection, query: &str, limit: i32) -> Result<Value, String> {
trace!("search_products: query={query}, limit={limit}");

View file

@ -539,6 +539,50 @@ impl BotModelsClient {
Ok(result.text)
}
pub async fn scan_barcode(
&self,
image_url_or_path: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
if !self.config.enabled {
return Err("BotModels is not enabled".into());
}
let url = format!("{}/api/vision/barcode", self.config.base_url());
trace!("Scanning barcode at {}: {}", url, image_url_or_path);
let image_data = if image_url_or_path.starts_with("http") {
let response = self.client.get(image_url_or_path).send().await?;
response.bytes().await?.to_vec()
} else {
tokio::fs::read(image_url_or_path).await?
};
let form = reqwest::multipart::Form::new().part(
"file",
reqwest::multipart::Part::bytes(image_data)
.file_name("image.png")
.mime_str("image/png")?,
);
let response = self
.client
.post(&url)
.header("X-API-Key", &self.config.api_key)
.multipart(form)
.send()
.await?;
if !response.status().is_success() {
let error_text = response.text().await.unwrap_or_default();
error!("Barcode scan failed: {}", error_text);
return Err(format!("Barcode scan failed: {}", error_text).into());
}
let result: serde_json::Value = response.json().await?;
info!("Barcode scanned: {:?}", result);
Ok(result.to_string())
}
pub async fn health_check(&self) -> bool {
if !self.config.enabled {
return false;