From e4524d0584104778ca6828c9686e8b65320837b4 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Wed, 14 Jan 2026 11:43:34 -0300 Subject: [PATCH] Add SCAN BARCODE keyword and BotModelsClient.scan_barcode --- src/basic/keywords/products.rs | 93 ++++++++++++++++++++++++++++++++++ src/multimodal/mod.rs | 44 ++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/src/basic/keywords/products.rs b/src/basic/keywords/products.rs index cc2a3942..e51460d2 100644 --- a/src/basic/keywords/products.rs +++ b/src/basic/keywords/products.rs @@ -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 { @@ -237,6 +315,21 @@ fn get_product_by_id(conn: &mut diesel::PgConnection, id: i64) -> Result Result> { + 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 { trace!("search_products: query={query}, limit={limit}"); diff --git a/src/multimodal/mod.rs b/src/multimodal/mod.rs index 1a287b1c..24860472 100644 --- a/src/multimodal/mod.rs +++ b/src/multimodal/mod.rs @@ -539,6 +539,50 @@ impl BotModelsClient { Ok(result.text) } + pub async fn scan_barcode( + &self, + image_url_or_path: &str, + ) -> Result> { + 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;