Compare commits
2 commits
0f0ea3e137
...
9a4bab6de6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9a4bab6de6 | ||
![]() |
e17e0e36a0 |
12 changed files with 258 additions and 430 deletions
59
src/main.rs
59
src/main.rs
|
@ -3,54 +3,25 @@ use actix_web::http::header;
|
||||||
use actix_web::{web, App, HttpServer};
|
use actix_web::{web, App, HttpServer};
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
|
|
||||||
use services::script
|
|
||||||
::*;
|
|
||||||
use services::config::*;
|
use services::config::*;
|
||||||
use services::email::*;
|
use services::email::*;
|
||||||
use services::file::*;
|
use services::file::*;
|
||||||
use services::state::*;
|
|
||||||
use services::llm::*;
|
use services::llm::*;
|
||||||
|
use services::script::*;
|
||||||
|
use services::state::*;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
//use services:: find::*;
|
//use services:: find::*;
|
||||||
mod services;
|
mod services;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
|
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
let config = AppConfig::from_env();
|
let config = AppConfig::from_env();
|
||||||
|
|
||||||
|
|
||||||
// let table_str = "rob";
|
|
||||||
// let filter_str = "ACTION=EMUL1";
|
|
||||||
|
|
||||||
// match execute_find(table_str, filter_str) {
|
|
||||||
// Ok(result) => println!("{}", result),
|
|
||||||
// Err(e) => eprintln!("Error: {}", e),
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
let script_service = ScriptService::new();
|
|
||||||
|
|
||||||
let script = r#"
|
|
||||||
let items = FIND "rob", "ACTION=EMUL1"
|
|
||||||
FOR EACH item IN items
|
|
||||||
let text = GET "example.com"
|
|
||||||
PRINT item.name
|
|
||||||
NEXT item "#;
|
|
||||||
|
|
||||||
match script_service.compile(script) {
|
|
||||||
Ok(ast) => {
|
|
||||||
match script_service.run(&ast) {
|
|
||||||
Ok(result) => println!("Script executed successfully: {:?}", result),
|
|
||||||
Err(e) => eprintln!("Error executing script: {}", e),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => eprintln!("Error compiling script: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let db_url = config.database_url();
|
let db_url = config.database_url();
|
||||||
|
let db_custom_url = config.database_custom_url();
|
||||||
let db = PgPool::connect(&db_url).await.unwrap();
|
let db = PgPool::connect(&db_url).await.unwrap();
|
||||||
|
let db_custom = PgPool::connect(&db_custom_url).await.unwrap();
|
||||||
|
|
||||||
let minio_client = init_minio(&config)
|
let minio_client = init_minio(&config)
|
||||||
.await
|
.await
|
||||||
|
@ -58,20 +29,34 @@ async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
let app_state = web::Data::new(AppState {
|
let app_state = web::Data::new(AppState {
|
||||||
db: db.into(),
|
db: db.into(),
|
||||||
|
db_custom: db_custom.into(),
|
||||||
config: Some(config.clone()),
|
config: Some(config.clone()),
|
||||||
minio_client: minio_client.into(),
|
minio_client: minio_client.into(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let script_service = ScriptService::new(&app_state.clone());
|
||||||
|
|
||||||
|
const TEXT : &str = include_str!("prompts/business/data-enrichment.bas");
|
||||||
|
|
||||||
|
match script_service.compile(TEXT) {
|
||||||
|
Ok(ast) => match script_service.run(&ast) {
|
||||||
|
Ok(result) => println!("Script executed successfully: {:?}", result),
|
||||||
|
Err(e) => eprintln!("Error executing script: {}", e),
|
||||||
|
},
|
||||||
|
Err(e) => eprintln!("Error compiling script: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
// Start HTTP server
|
// Start HTTP server
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let cors = Cors::default()
|
let cors = Cors::default()
|
||||||
.allowed_origin("http://localhost:3000") // Your Next.js port
|
.send_wildcard()
|
||||||
|
.allowed_origin("*")
|
||||||
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
|
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
|
||||||
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
|
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
|
||||||
.allowed_header(header::CONTENT_TYPE)
|
.allowed_header(header::CONTENT_TYPE)
|
||||||
.max_age(3600);
|
.max_age(3600);
|
||||||
App::new()
|
App::new()
|
||||||
//.wrap(cors)
|
.wrap(cors)
|
||||||
.app_data(app_state.clone())
|
.app_data(app_state.clone())
|
||||||
.service(upload_file)
|
.service(upload_file)
|
||||||
.service(list_file)
|
.service(list_file)
|
||||||
|
|
5
src/prompts/business/data-enrichment.bas
Normal file
5
src/prompts/business/data-enrichment.bas
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
let items = FIND "gb.rob", "ACTION=EMUL1"
|
||||||
|
FOR EACH item IN items
|
||||||
|
let text = GET "example.com"
|
||||||
|
PRINT item.name
|
||||||
|
NEXT item
|
|
@ -2,6 +2,7 @@ pub mod config;
|
||||||
|
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod email;
|
pub mod email;
|
||||||
|
pub mod keywords;
|
||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod llm;
|
pub mod llm;
|
||||||
pub mod script;
|
pub mod script;
|
|
@ -5,6 +5,8 @@ pub struct AppConfig {
|
||||||
pub minio: MinioConfig,
|
pub minio: MinioConfig,
|
||||||
pub server: ServerConfig,
|
pub server: ServerConfig,
|
||||||
pub database: DatabaseConfig,
|
pub database: DatabaseConfig,
|
||||||
|
pub database_custom: DatabaseConfig,
|
||||||
|
|
||||||
pub email: EmailConfig,
|
pub email: EmailConfig,
|
||||||
pub ai: AIConfig,
|
pub ai: AIConfig,
|
||||||
}
|
}
|
||||||
|
@ -40,16 +42,12 @@ pub struct EmailConfig {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub reject_unauthorized: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AIConfig {
|
pub struct AIConfig {
|
||||||
pub image_model: String,
|
|
||||||
pub embedding_model: String,
|
|
||||||
pub instance: String,
|
pub instance: String,
|
||||||
pub key: String,
|
pub key: String,
|
||||||
pub llm_model: String,
|
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub endpoint: String,
|
pub endpoint: String,
|
||||||
}
|
}
|
||||||
|
@ -67,6 +65,18 @@ impl AppConfig {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn database_custom_url(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"postgres://{}:{}@{}:{}/{}",
|
||||||
|
self.database_custom.username,
|
||||||
|
self.database_custom.password,
|
||||||
|
self.database_custom.server,
|
||||||
|
self.database_custom.port,
|
||||||
|
self.database_custom.database
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn from_env() -> Self {
|
pub fn from_env() -> Self {
|
||||||
let database = DatabaseConfig {
|
let database = DatabaseConfig {
|
||||||
username: env::var("TABLES_USERNAME").unwrap_or_else(|_| "user".to_string()),
|
username: env::var("TABLES_USERNAME").unwrap_or_else(|_| "user".to_string()),
|
||||||
|
@ -79,6 +89,17 @@ impl AppConfig {
|
||||||
database: env::var("TABLES_DATABASE").unwrap_or_else(|_| "db".to_string()),
|
database: env::var("TABLES_DATABASE").unwrap_or_else(|_| "db".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let database_custom = DatabaseConfig {
|
||||||
|
username: env::var("CUSTOM_USERNAME").unwrap_or_else(|_| "user".to_string()),
|
||||||
|
password: env::var("CUSTOM_PASSWORD").unwrap_or_else(|_| "pass".to_string()),
|
||||||
|
server: env::var("CUSTOM_SERVER").unwrap_or_else(|_| "localhost".to_string()),
|
||||||
|
port: env::var("CUSTOM_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.parse().ok())
|
||||||
|
.unwrap_or(5432),
|
||||||
|
database: env::var("CUSTOM_DATABASE").unwrap_or_else(|_| "db".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
let minio = MinioConfig {
|
let minio = MinioConfig {
|
||||||
server: env::var("DRIVE_SERVER").expect("DRIVE_SERVER not set"),
|
server: env::var("DRIVE_SERVER").expect("DRIVE_SERVER not set"),
|
||||||
access_key: env::var("DRIVE_ACCESSKEY").expect("DRIVE_ACCESSKEY not set"),
|
access_key: env::var("DRIVE_ACCESSKEY").expect("DRIVE_ACCESSKEY not set"),
|
||||||
|
@ -99,18 +120,11 @@ impl AppConfig {
|
||||||
.expect("EMAIL_PORT must be a number"),
|
.expect("EMAIL_PORT must be a number"),
|
||||||
username: env::var("EMAIL_USER").expect("EMAIL_USER not set"),
|
username: env::var("EMAIL_USER").expect("EMAIL_USER not set"),
|
||||||
password: env::var("EMAIL_PASS").expect("EMAIL_PASS not set"),
|
password: env::var("EMAIL_PASS").expect("EMAIL_PASS not set"),
|
||||||
reject_unauthorized: env::var("EMAIL_REJECT_UNAUTHORIZED")
|
|
||||||
.unwrap_or_else(|_| "false".to_string())
|
|
||||||
.parse()
|
|
||||||
.unwrap_or(false),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let ai = AIConfig {
|
let ai = AIConfig {
|
||||||
image_model: env::var("AI_IMAGE_MODEL").expect("AI_IMAGE_MODEL not set"),
|
|
||||||
embedding_model: env::var("AI_EMBEDDING_MODEL").expect("AI_EMBEDDING_MODEL not set"),
|
|
||||||
instance: env::var("AI_INSTANCE").expect("AI_INSTANCE not set"),
|
instance: env::var("AI_INSTANCE").expect("AI_INSTANCE not set"),
|
||||||
key: env::var("AI_KEY").expect("AI_KEY not set"),
|
key: env::var("AI_KEY").expect("AI_KEY not set"),
|
||||||
llm_model: env::var("AI_LLM_MODEL").expect("AI_LLM_MODEL not set"),
|
|
||||||
version: env::var("AI_VERSION").expect("AI_VERSION not set"),
|
version: env::var("AI_VERSION").expect("AI_VERSION not set"),
|
||||||
endpoint: env::var("AI_ENDPOINT").expect("AI_ENDPOINT not set"),
|
endpoint: env::var("AI_ENDPOINT").expect("AI_ENDPOINT not set"),
|
||||||
};
|
};
|
||||||
|
@ -125,6 +139,7 @@ impl AppConfig {
|
||||||
.unwrap_or(8080),
|
.unwrap_or(8080),
|
||||||
},
|
},
|
||||||
database,
|
database,
|
||||||
|
database_custom,
|
||||||
email,
|
email,
|
||||||
ai,
|
ai,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
|
|
||||||
|
|
||||||
use actix_web::{ web};
|
use actix_web::{ web};
|
||||||
|
|
||||||
use actix_multipart::Multipart;
|
use actix_multipart::Multipart;
|
||||||
|
|
|
@ -1,232 +0,0 @@
|
||||||
- 100KB Email box
|
|
||||||
|
|
||||||
i need a vb to russt watx
|
|
||||||
first wkeywords;
|
|
||||||
the keyword find for example will call the websiervics speciifid.
|
|
||||||
you now ? this should be compatible with languageserer so can be debuger
|
|
||||||
in code. user rust minamalistc to do this very inline and minimize code verbosity
|
|
||||||
it will be integatd in antoher proejt so i nee a sevicelayer
|
|
||||||
to call it from a schduler i need the compiler and the rnner of the ob
|
|
||||||
use localhost:5858 to call websiervis from the wasm executable
|
|
||||||
this is will attend
|
|
||||||
|
|
||||||
FOR EACH item in ARRAY
|
|
||||||
next item
|
|
||||||
|
|
||||||
FIND filter
|
|
||||||
|
|
||||||
json = FIND "tablename", "field=value"
|
|
||||||
/tables/find
|
|
||||||
|
|
||||||
SET "tablename", "key=value", "value"
|
|
||||||
/tables/set
|
|
||||||
|
|
||||||
text = GET "domain.com"
|
|
||||||
/webauto/get_text
|
|
||||||
|
|
||||||
text = GET WEBSITE "CASAS BAHIA" 'casasbahia.com via duckduckgo.
|
|
||||||
/webauto/get_website
|
|
||||||
|
|
||||||
CREATE SITE "sitename",company, website, template, prompt
|
|
||||||
|
|
||||||
/sites/create
|
|
||||||
copy template from temapltes folder
|
|
||||||
add llm outpt page.tsx
|
|
||||||
add listener
|
|
||||||
|
|
||||||
CREATE DRAFT to, subject, body
|
|
||||||
/email/create_draft
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
use serde_json::{json, Value};
|
|
||||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
|
||||||
use sqlx::Column; // Required for .name() method
|
|
||||||
use sqlx::TypeInfo; // Required for .type_info() method
|
|
||||||
use std::error::Error;
|
|
||||||
use sqlx::postgres::PgPoolOptions;
|
|
||||||
|
|
||||||
// Main async function to execute the FIND command
|
|
||||||
pub async fn execute_find(
|
|
||||||
pool: &PgPool,
|
|
||||||
table_str: &str,
|
|
||||||
filter_str: &str,
|
|
||||||
) -> Result<Value, Box<dyn Error>> {
|
|
||||||
// Parse the filter string into SQL WHERE clause and parameters
|
|
||||||
let (where_clause, params) = parse_filter(filter_str)?;
|
|
||||||
|
|
||||||
// Build the SQL query with proper parameter binding
|
|
||||||
let query = format!("SELECT * FROM {} WHERE {}", table_str, where_clause);
|
|
||||||
|
|
||||||
// Execute query and collect results
|
|
||||||
let rows = match params.len() {
|
|
||||||
0 => sqlx::query(&query).fetch_all(pool).await?,
|
|
||||||
1 => sqlx::query(&query).bind(¶ms[0]).fetch_all(pool).await?,
|
|
||||||
_ => return Err("Only single parameter filters supported in this example".into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert rows to JSON values
|
|
||||||
let mut results = Vec::new();
|
|
||||||
for row in rows {
|
|
||||||
results.push(row_to_json(row)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the structured result
|
|
||||||
Ok(json!({
|
|
||||||
"command": "find",
|
|
||||||
"table": table_str,
|
|
||||||
"filter": filter_str,
|
|
||||||
"results": results
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row_to_json(row: PgRow) -> Result<Value, Box<dyn Error>> {
|
|
||||||
let mut result = serde_json::Map::new();
|
|
||||||
for (i, column) in row.columns().iter().enumerate() {
|
|
||||||
let column_name = column.name();
|
|
||||||
let value: Value = match column.type_info().name() {
|
|
||||||
"int4" | "int8" => {
|
|
||||||
match row.try_get::<i64, _>(i) {
|
|
||||||
Ok(v) => json!(v),
|
|
||||||
Err(_) => Value::Null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"float4" | "float8" => {
|
|
||||||
match row.try_get::<f64, _>(i) {
|
|
||||||
Ok(v) => json!(v),
|
|
||||||
Err(_) => Value::Null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"text" | "varchar" => {
|
|
||||||
match row.try_get::<String, _>(i) {
|
|
||||||
Ok(v) => json!(v),
|
|
||||||
Err(_) => Value::Null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bool" => {
|
|
||||||
match row.try_get::<bool, _>(i) {
|
|
||||||
Ok(v) => json!(v),
|
|
||||||
Err(_) => Value::Null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"json" | "jsonb" => {
|
|
||||||
match row.try_get::<Value, _>(i) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => Value::Null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => Value::Null,
|
|
||||||
};
|
|
||||||
result.insert(column_name.to_string(), value);
|
|
||||||
}
|
|
||||||
Ok(Value::Object(result))
|
|
||||||
}
|
|
||||||
// Helper function to parse the filter string into SQL WHERE clause and parameters
|
|
||||||
fn parse_filter(filter_str: &str) -> Result<(String, Vec<String>), Box<dyn Error>> {
|
|
||||||
let parts: Vec<&str> = filter_str.split('=').collect();
|
|
||||||
if parts.len() != 2 {
|
|
||||||
return Err("Invalid filter format. Expected 'KEY=VALUE'".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let column = parts[0].trim();
|
|
||||||
let value = parts[1].trim();
|
|
||||||
|
|
||||||
// Validate column name to prevent SQL injection
|
|
||||||
if !column.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
|
|
||||||
return Err("Invalid column name in filter".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the parameterized query part and the value separately
|
|
||||||
Ok((format!("{} = $1", column), vec![value.to_string()]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Database connection setup
|
|
||||||
pub async fn create_pool(database_url: &str) -> Result<PgPool, Box<dyn Error>> {
|
|
||||||
let pool = PgPoolOptions::new()
|
|
||||||
.max_connections(5)
|
|
||||||
.connect(database_url)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Test the connection
|
|
||||||
sqlx::query("SELECT 1").execute(&pool).await?;
|
|
||||||
|
|
||||||
Ok(pool)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use sqlx::postgres::PgPoolOptions;
|
|
||||||
use dotenv::dotenv;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
async fn setup_test_db() -> PgPool {
|
|
||||||
dotenv().ok();
|
|
||||||
let database_url = env::var("DATABASE_URL")
|
|
||||||
.expect("DATABASE_URL must be set in .env for tests");
|
|
||||||
|
|
||||||
let pool = PgPoolOptions::new()
|
|
||||||
.max_connections(1)
|
|
||||||
.connect(&database_url)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Create a test table
|
|
||||||
sqlx::query(
|
|
||||||
r#"
|
|
||||||
DROP TABLE IF EXISTS rob;
|
|
||||||
CREATE TABLE rob (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
action TEXT,
|
|
||||||
name TEXT,
|
|
||||||
is_active BOOLEAN,
|
|
||||||
metadata JSONB
|
|
||||||
)
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
.execute(&pool)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Insert test data
|
|
||||||
sqlx::query(
|
|
||||||
r#"
|
|
||||||
INSERT INTO rob (action, name, is_active, metadata) VALUES
|
|
||||||
('EMUL1', 'Robot1', true, '{"version": 1}'),
|
|
||||||
('EMUL2', 'Robot2', false, '{"version": 2}'),
|
|
||||||
('EMUL1', 'Robot3', true, null)
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
.execute(&pool)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
pool
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_execute_find() {
|
|
||||||
let pool = setup_test_db().await;
|
|
||||||
let result = execute_find(&pool, "rob", "action=EMUL1")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let results = result["results"].as_array().unwrap();
|
|
||||||
assert_eq!(results.len(), 2);
|
|
||||||
assert_eq!(results[0]["action"], "EMUL1");
|
|
||||||
assert_eq!(results[1]["action"], "EMUL1");
|
|
||||||
|
|
||||||
// Test JSON field
|
|
||||||
assert_eq!(results[0]["metadata"]["version"], 1);
|
|
||||||
|
|
||||||
// Test boolean field
|
|
||||||
assert_eq!(results[0]["is_active"], true);
|
|
||||||
}
|
|
||||||
}
|
|
156
src/services/keywords/find.rs
Normal file
156
src/services/keywords/find.rs
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use sqlx::Column; // Required for .name() method
|
||||||
|
use sqlx::TypeInfo; // Required for .type_info() method
|
||||||
|
use sqlx::{postgres::PgRow, PgPool, Row};
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn execute_find(
|
||||||
|
pool: &PgPool,
|
||||||
|
table_str: &str,
|
||||||
|
filter_str: &str,
|
||||||
|
) -> Result<Value, String> { // Changed to String error like your Actix code
|
||||||
|
println!("Starting execute_find with table: {}, filter: {}", table_str, filter_str);
|
||||||
|
|
||||||
|
let (where_clause, params) = parse_filter(filter_str)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let query = format!("SELECT * FROM {} WHERE {} LIMIT 10", table_str, where_clause);
|
||||||
|
println!("Executing query: {}", query);
|
||||||
|
|
||||||
|
// Use the same simple pattern as your Actix code - no timeout wrapper
|
||||||
|
let rows = sqlx::query(&query)
|
||||||
|
.bind(¶ms[0]) // Simplified like your working code
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("SQL execution error: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
println!("Query successful, got {} rows", rows.len());
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
results.push(row_to_json(row).map_err(|e| e.to_string())?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"command": "find",
|
||||||
|
"table": table_str,
|
||||||
|
"filter": filter_str,
|
||||||
|
"results": results
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_to_json(row: PgRow) -> Result<Value, Box<dyn Error>> {
|
||||||
|
let mut result = serde_json::Map::new();
|
||||||
|
let columns = row.columns();
|
||||||
|
println!("Processing {} columns", columns.len());
|
||||||
|
|
||||||
|
for (i, column) in columns.iter().enumerate() {
|
||||||
|
let column_name = column.name();
|
||||||
|
let type_name = column.type_info().name();
|
||||||
|
println!(
|
||||||
|
"Processing column {}: {} (type: {})",
|
||||||
|
i, column_name, type_name
|
||||||
|
);
|
||||||
|
|
||||||
|
let value: Value = match type_name {
|
||||||
|
"INT4" | "INT8" | "int4" | "int8" => match row.try_get::<i64, _>(i) {
|
||||||
|
Ok(v) => {
|
||||||
|
println!("Got int64 value: {}", v);
|
||||||
|
json!(v)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get int64, trying i32: {}", e);
|
||||||
|
match row.try_get::<i32, _>(i) {
|
||||||
|
Ok(v) => json!(v as i64),
|
||||||
|
Err(_) => Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FLOAT4" | "FLOAT8" | "float4" | "float8" => match row.try_get::<f64, _>(i) {
|
||||||
|
Ok(v) => {
|
||||||
|
println!("Got float64 value: {}", v);
|
||||||
|
json!(v)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get float64, trying f32: {}", e);
|
||||||
|
match row.try_get::<f32, _>(i) {
|
||||||
|
Ok(v) => json!(v as f64),
|
||||||
|
Err(_) => Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TEXT" | "VARCHAR" | "text" | "varchar" => match row.try_get::<String, _>(i) {
|
||||||
|
Ok(v) => {
|
||||||
|
println!("Got string value: {}", v);
|
||||||
|
json!(v)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get string: {}", e);
|
||||||
|
Value::Null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BOOL" | "bool" => match row.try_get::<bool, _>(i) {
|
||||||
|
Ok(v) => {
|
||||||
|
println!("Got bool value: {}", v);
|
||||||
|
json!(v)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get bool: {}", e);
|
||||||
|
Value::Null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"JSON" | "JSONB" | "json" | "jsonb" => match row.try_get::<Value, _>(i) {
|
||||||
|
Ok(v) => {
|
||||||
|
println!("Got JSON value: {:?}", v);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get JSON, trying as string: {}", e);
|
||||||
|
match row.try_get::<String, _>(i) {
|
||||||
|
Ok(s) => match serde_json::from_str(&s) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => json!(s),
|
||||||
|
},
|
||||||
|
Err(_) => Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
println!("Unknown type {}, trying as string", type_name);
|
||||||
|
match row.try_get::<String, _>(i) {
|
||||||
|
Ok(v) => json!(v),
|
||||||
|
Err(_) => Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
result.insert(column_name.to_string(), value);
|
||||||
|
}
|
||||||
|
println!("Finished processing row, got {} fields", result.len());
|
||||||
|
Ok(Value::Object(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse the filter string into SQL WHERE clause and parameters
|
||||||
|
fn parse_filter(filter_str: &str) -> Result<(String, Vec<String>), Box<dyn Error>> {
|
||||||
|
let parts: Vec<&str> = filter_str.split('=').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err("Invalid filter format. Expected 'KEY=VALUE'".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let column = parts[0].trim();
|
||||||
|
let value = parts[1].trim();
|
||||||
|
|
||||||
|
// Validate column name to prevent SQL injection
|
||||||
|
if !column
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||||
|
{
|
||||||
|
return Err("Invalid column name in filter".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the parameterized query part and the value separately
|
||||||
|
Ok((format!("{} = $1", column), vec![value.to_string()]))
|
||||||
|
}
|
1
src/services/keywords/mod.rs
Normal file
1
src/services/keywords/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod find;
|
|
@ -16,7 +16,6 @@ use langchain_rust::{
|
||||||
template_fstring,
|
template_fstring,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
use crate::services::{config::AIConfig, state::AppState};
|
use crate::services::{config::AIConfig, state::AppState};
|
||||||
|
|
||||||
pub fn from_config(config: &AIConfig) -> AzureConfig {
|
pub fn from_config(config: &AIConfig) -> AzureConfig {
|
||||||
|
@ -30,9 +29,8 @@ pub fn from_config(config: &AIConfig) -> AzureConfig {
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct ChatRequest {
|
struct ChatRequest {
|
||||||
input: String,
|
input: String,
|
||||||
context: String,
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
struct ChatResponse {
|
struct ChatResponse {
|
||||||
text: String,
|
text: String,
|
||||||
|
@ -58,11 +56,14 @@ pub async fn chat(
|
||||||
// Parse the context JSON
|
// Parse the context JSON
|
||||||
let context: serde_json::Value = match serde_json::from_str(&request) {
|
let context: serde_json::Value = match serde_json::from_str(&request) {
|
||||||
Ok(ctx) => ctx,
|
Ok(ctx) => ctx,
|
||||||
Err(_) => serde_json::json!({})
|
Err(_) => serde_json::json!({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check view type and prepare appropriate prompt
|
// Check view type and prepare appropriate prompt
|
||||||
let view_type = context.get("viewType").and_then(|v| v.as_str()).unwrap_or("");
|
let view_type = context
|
||||||
|
.get("viewType")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
let (prompt, might_trigger_action) = match view_type {
|
let (prompt, might_trigger_action) = match view_type {
|
||||||
"email" => (
|
"email" => (
|
||||||
format!(
|
format!(
|
||||||
|
@ -109,7 +110,6 @@ pub async fn chat_stream(
|
||||||
let azure_config = from_config(&state.config.clone().unwrap().ai);
|
let azure_config = from_config(&state.config.clone().unwrap().ai);
|
||||||
let open_ai = OpenAI::new(azure_config);
|
let open_ai = OpenAI::new(azure_config);
|
||||||
|
|
||||||
|
|
||||||
let prompt = message_formatter![
|
let prompt = message_formatter![
|
||||||
fmt_message!(Message::new_system_message(
|
fmt_message!(Message::new_system_message(
|
||||||
"You are world class technical documentation writer."
|
"You are world class technical documentation writer."
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
use smartstring::SmartString;
|
use rhai::{Array, Dynamic, Engine};
|
||||||
use anyhow::Error;
|
use rhai::{EvalAltResult};
|
||||||
use rhai::module_resolvers::StaticModuleResolver;
|
|
||||||
use rhai::{Array, Dynamic, Engine, FnPtr, Scope};
|
|
||||||
use rhai::{EvalAltResult, ImmutableString, LexError, ParseError, ParseErrorType, Position};
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::collections::HashMap;
|
use smartstring::SmartString;
|
||||||
|
|
||||||
|
use crate::services::keywords::find::execute_find;
|
||||||
|
use crate::services::state::AppState;
|
||||||
|
|
||||||
pub struct ScriptService {
|
pub struct ScriptService {
|
||||||
engine: Engine,
|
engine: Engine,
|
||||||
module_resolver: StaticModuleResolver,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn json_value_to_dynamic(value: &Value) -> Dynamic {
|
fn json_value_to_dynamic(value: &Value) -> Dynamic {
|
||||||
|
@ -53,9 +53,8 @@ fn to_array(value: Dynamic) -> Array {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScriptService {
|
impl ScriptService {
|
||||||
pub fn new() -> Self {
|
pub fn new(state: &AppState) -> Self {
|
||||||
let mut engine = Engine::new();
|
let mut engine = Engine::new();
|
||||||
let module_resolver = StaticModuleResolver::new();
|
|
||||||
|
|
||||||
// Configure engine for BASIC-like syntax
|
// Configure engine for BASIC-like syntax
|
||||||
engine.set_allow_anonymous_fn(true);
|
engine.set_allow_anonymous_fn(true);
|
||||||
|
@ -107,7 +106,7 @@ impl ScriptService {
|
||||||
|
|
||||||
for item in array {
|
for item in array {
|
||||||
// Push the loop variable into the scope
|
// Push the loop variable into the scope
|
||||||
context.scope_mut().push(loop_var.clone(), item);
|
context.scope_mut().push(loop_var, item);
|
||||||
|
|
||||||
// Evaluate the block with the current scope
|
// Evaluate the block with the current scope
|
||||||
match context.eval_expression_tree(block) {
|
match context.eval_expression_tree(block) {
|
||||||
|
@ -140,50 +139,41 @@ impl ScriptService {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// FIND command: FIND "table", "filter"
|
// FIND command: FIND "table", "filter"
|
||||||
|
// Clone the database reference outside the closure to avoid lifetime issues
|
||||||
|
let db = state.db_custom.clone();
|
||||||
|
|
||||||
engine
|
engine
|
||||||
.register_custom_syntax(
|
.register_custom_syntax(&["FIND", "$expr$", ",", "$expr$"], false, {
|
||||||
&["FIND", "$expr$", ",", "$expr$"],
|
let db = db.clone();
|
||||||
false, // Expression, not statement
|
|
||||||
|context, inputs| {
|
move |context, inputs| {
|
||||||
let table_name = context.eval_expression_tree(&inputs[0])?;
|
let table_name = context.eval_expression_tree(&inputs[0])?;
|
||||||
let filter = context.eval_expression_tree(&inputs[1])?;
|
let filter = context.eval_expression_tree(&inputs[1])?;
|
||||||
|
let binding = db.as_ref().unwrap();
|
||||||
|
|
||||||
let table_str = table_name.to_string();
|
// Use the current async context instead of creating a new runtime
|
||||||
let filter_str = filter.to_string();
|
let binding2 = table_name.to_string();
|
||||||
|
let binding3 = filter.to_string();
|
||||||
|
let fut = execute_find(
|
||||||
|
binding,
|
||||||
|
&binding2,
|
||||||
|
&binding3,
|
||||||
|
);
|
||||||
|
|
||||||
use serde_json::json;
|
// Use tokio::task::block_in_place + tokio::runtime::Handle::current().block_on
|
||||||
|
let result = tokio::task::block_in_place(|| {
|
||||||
|
tokio::runtime::Handle::current().block_on(fut)
|
||||||
|
})
|
||||||
|
.map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
|
||||||
let result = json!({
|
if let Some(results) = result.get("results") {
|
||||||
"command": "find",
|
let array = to_array(json_value_to_dynamic(results));
|
||||||
"table": table_str,
|
|
||||||
"filter": filter_str,
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "dummy1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "dummy2"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if let serde_json::Value::Object(ref obj) = result {
|
|
||||||
if let Some(results_value) = obj.get("results") {
|
|
||||||
let dynamic_results = json_value_to_dynamic(results_value);
|
|
||||||
|
|
||||||
// Now you can work with it as Dynamic
|
|
||||||
let array = to_array(dynamic_results);
|
|
||||||
Ok(Dynamic::from(array))
|
Ok(Dynamic::from(array))
|
||||||
} else {
|
} else {
|
||||||
Err("No results found".into())
|
Err("No results".into())
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Err("Invalid result format".into())
|
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// SET command: SET "table", "key", "value"
|
// SET command: SET "table", "key", "value"
|
||||||
|
@ -305,7 +295,6 @@ impl ScriptService {
|
||||||
|
|
||||||
ScriptService {
|
ScriptService {
|
||||||
engine,
|
engine,
|
||||||
module_resolver,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,97 +398,4 @@ impl ScriptService {
|
||||||
pub fn run(&self, ast: &rhai::AST) -> Result<Dynamic, Box<EvalAltResult>> {
|
pub fn run(&self, ast: &rhai::AST) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||||
self.engine.eval_ast(ast)
|
self.engine.eval_ast(ast)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn call_web_service(
|
|
||||||
&self,
|
|
||||||
endpoint: &str,
|
|
||||||
data: HashMap<String, String>,
|
|
||||||
) -> Result<String, Box<EvalAltResult>> {
|
|
||||||
Ok(format!("Called {} with {:?}", endpoint, data))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute a BASIC-style script without semicolons
|
|
||||||
pub fn execute_basic_script(&self, script: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
|
||||||
let processed = self.preprocess_basic_script(script);
|
|
||||||
let ast = self.engine.compile(&processed)?;
|
|
||||||
self.run(&ast)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic_script_without_semicolons() {
|
|
||||||
let service = ScriptService::new();
|
|
||||||
|
|
||||||
// Test BASIC-style script without semicolons
|
|
||||||
let script = r#"
|
|
||||||
json = FIND "users", "name=John"
|
|
||||||
SET "users", "name=John", "age=30"
|
|
||||||
text = GET "example.com"
|
|
||||||
CREATE SITE "mysite", "My Company", "mycompany.com", "basic", "Create a professional site"
|
|
||||||
CREATE DRAFT "client@example.com", "Project Update", "Here's the latest update..."
|
|
||||||
PRINT "Script completed successfully"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let result = service.execute_basic_script(script);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_preprocessing() {
|
|
||||||
let service = ScriptService::new();
|
|
||||||
|
|
||||||
let script = r#"
|
|
||||||
json = FIND "users", "name=John"
|
|
||||||
SET "users", "name=John", "age=30"
|
|
||||||
let x = 42
|
|
||||||
PRINT x
|
|
||||||
if x > 10 {
|
|
||||||
PRINT "Large number"
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let processed = service.preprocess_basic_script(script);
|
|
||||||
|
|
||||||
// Should add semicolons to regular statements but not custom commands
|
|
||||||
assert!(processed.contains("let x = 42;"));
|
|
||||||
assert!(processed.contains("json = FIND"));
|
|
||||||
assert!(!processed.contains("SET \"users\""));
|
|
||||||
assert!(!processed.contains("PRINT \"Large number\";")); // Inside block shouldn't get semicolon
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_individual_commands() {
|
|
||||||
let service = ScriptService::new();
|
|
||||||
|
|
||||||
let commands = vec![
|
|
||||||
r#"SET "users", "name=John", "age=30""#,
|
|
||||||
r#"CREATE SITE "mysite", "My Company", "mycompany.com", "basic", "Create a professional site""#,
|
|
||||||
r#"CREATE DRAFT "client@example.com", "Project Update", "Here's the latest update...""#,
|
|
||||||
r#"PRINT "Hello, World!""#,
|
|
||||||
];
|
|
||||||
|
|
||||||
for cmd in commands {
|
|
||||||
let result = service.execute_basic_script(cmd);
|
|
||||||
assert!(result.is_ok(), "Command '{}' failed", cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_block_statements() {
|
|
||||||
let service = ScriptService::new();
|
|
||||||
|
|
||||||
let script = r#"
|
|
||||||
if true {
|
|
||||||
PRINT "Inside block"
|
|
||||||
PRINT "Another statement"
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let result = service.execute_basic_script(script);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,6 @@ pub struct AppState {
|
||||||
pub minio_client: Option<Client>,
|
pub minio_client: Option<Client>,
|
||||||
pub config: Option<AppConfig>,
|
pub config: Option<AppConfig>,
|
||||||
pub db: Option<sqlx::PgPool>,
|
pub db: Option<sqlx::PgPool>,
|
||||||
|
pub db_custom: Option<sqlx::PgPool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue