Compare commits

...

2 commits

Author SHA1 Message Date
45ab675b21 Move models into shared module and clean imports
Relocate bot and organization models into shared module, update imports
and remove unused modules.
2025-10-05 10:20:42 -03:00
ac716981ec - New paths, removing compiling errors. 2025-10-05 09:47:49 -03:00
108 changed files with 160 additions and 608 deletions

View file

@ -1,6 +1,6 @@
use crate::models::automation_model::{Automation, TriggerKind}; use crate::models::automation_model::{Automation, TriggerKind};
use crate::services::script::ScriptService; use crate::basic::ScriptService;
use crate::services::state::AppState; use crate::state::AppState;
use chrono::Datelike; use chrono::Datelike;
use chrono::Timelike; use chrono::Timelike;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};

View file

@ -1,6 +1,6 @@
use crate::services::email::save_email_draft; use crate::email::save_email_draft;
use crate::services::email::{fetch_latest_sent_to, SaveDraftRequest}; use crate::email::{fetch_latest_sent_to, SaveDraftRequest};
use crate::services::state::AppState; use crate::state::AppState;
use rhai::Dynamic; use rhai::Dynamic;
use rhai::Engine; use rhai::Engine;

View file

@ -7,8 +7,8 @@ use std::fs;
use std::io::Read; use std::io::Read;
use std::path::PathBuf; use std::path::PathBuf;
use crate::services::state::AppState; use crate::state::AppState;
use crate::services::utils; use crate::utils;
pub fn create_site_keyword(state: &AppState, engine: &mut Engine) { pub fn create_site_keyword(state: &AppState, engine: &mut Engine) {
let state_clone = state.clone(); let state_clone = state.clone();
@ -43,7 +43,7 @@ pub fn create_site_keyword(state: &AppState, engine: &mut Engine) {
} }
async fn create_site( async fn create_site(
config: &crate::services::config::AppConfig, config: &crate::config::AppConfig,
alias: Dynamic, alias: Dynamic,
template_dir: Dynamic, template_dir: Dynamic,
prompt: Dynamic, prompt: Dynamic,

View file

@ -4,10 +4,10 @@ use rhai::Engine;
use serde_json::{json, Value}; use serde_json::{json, Value};
use sqlx::PgPool; use sqlx::PgPool;
use crate::services::state::AppState; use crate::state::AppState;
use crate::services::utils; use crate::utils;
use crate::services::utils::row_to_json; use crate::utils::row_to_json;
use crate::services::utils::to_array; use crate::utils::to_array;
pub fn find_keyword(state: &AppState, engine: &mut Engine) { pub fn find_keyword(state: &AppState, engine: &mut Engine) {
let db = state.db_custom.clone(); let db = state.db_custom.clone();

View file

@ -1,4 +1,4 @@
use crate::services::state::AppState; use crate::state::AppState;
use log::info; use log::info;
use rhai::Dynamic; use rhai::Dynamic;
use rhai::Engine; use rhai::Engine;

View file

@ -1,6 +1,6 @@
use log::info; use log::info;
use crate::services::state::AppState; use crate::state::AppState;
use reqwest::{self, Client}; use reqwest::{self, Client};
use rhai::{Dynamic, Engine}; use rhai::{Dynamic, Engine};
use scraper::{Html, Selector}; use scraper::{Html, Selector};

View file

@ -1,4 +1,4 @@
use crate::services::{state::AppState, web_automation::BrowserPool}; use crate::{state::AppState, web_automation::BrowserPool};
use log::info; use log::info;
use rhai::{Dynamic, Engine}; use rhai::{Dynamic, Engine};
use std::error::Error; use std::error::Error;

View file

@ -1,6 +1,6 @@
use log::info; use log::info;
use crate::services::{state::AppState, utils::call_llm}; use crate::{state::AppState, utils::call_llm};
use rhai::{Dynamic, Engine}; use rhai::{Dynamic, Engine};
pub fn llm_keyword(state: &AppState, engine: &mut Engine) { pub fn llm_keyword(state: &AppState, engine: &mut Engine) {

View file

@ -5,7 +5,7 @@ use serde_json::{json, Value};
use sqlx::PgPool; use sqlx::PgPool;
use crate::models::automation_model::TriggerKind; use crate::models::automation_model::TriggerKind;
use crate::services::state::AppState; use crate::state::AppState;
pub fn on_keyword(state: &AppState, engine: &mut Engine) { pub fn on_keyword(state: &AppState, engine: &mut Engine) {
let db = state.db_custom.clone(); let db = state.db_custom.clone();

View file

@ -2,7 +2,7 @@ use log::info;
use rhai::Dynamic; use rhai::Dynamic;
use rhai::Engine; use rhai::Engine;
use crate::services::state::AppState; use crate::state::AppState;
pub fn print_keyword(_state: &AppState, engine: &mut Engine) { pub fn print_keyword(_state: &AppState, engine: &mut Engine) {
// PRINT command // PRINT command

View file

@ -5,8 +5,8 @@ use serde_json::{json, Value};
use sqlx::PgPool; use sqlx::PgPool;
use std::error::Error; use std::error::Error;
use crate::services::state::AppState; use crate::state::AppState;
use crate::services::utils; use crate::utils;
pub fn set_keyword(state: &AppState, engine: &mut Engine) { pub fn set_keyword(state: &AppState, engine: &mut Engine) {
let db = state.db_custom.clone(); let db = state.db_custom.clone();

View file

@ -5,7 +5,7 @@ use serde_json::{json, Value};
use sqlx::PgPool; use sqlx::PgPool;
use crate::models::automation_model::TriggerKind; use crate::models::automation_model::TriggerKind;
use crate::services::state::AppState; use crate::state::AppState;
pub fn set_schedule_keyword(state: &AppState, engine: &mut Engine) { pub fn set_schedule_keyword(state: &AppState, engine: &mut Engine) {
let db = state.db_custom.clone(); let db = state.db_custom.clone();

View file

@ -1,4 +1,4 @@
use crate::services::state::AppState; use crate::state::AppState;
use log::info; use log::info;
use rhai::{Dynamic, Engine}; use rhai::{Dynamic, Engine};
use std::thread; use std::thread;

View file

@ -1,19 +1,21 @@
use crate::services::keywords::create_draft::create_draft_keyword; mod keywords;
use crate::services::keywords::create_site::create_site_keyword;
use crate::services::keywords::find::find_keyword; use self::keywords::create_draft::create_draft_keyword;
use crate::services::keywords::first::first_keyword; use self::keywords::create_site::create_site_keyword;
use crate::services::keywords::last::last_keyword; use self::keywords::find::find_keyword;
use crate::services::keywords::format::format_keyword; use self::keywords::first::first_keyword;
use crate::services::keywords::for_next::for_keyword; use self::keywords::for_next::for_keyword;
use crate::services::keywords::get::get_keyword; use self::keywords::format::format_keyword;
use crate::services::keywords::get_website::get_website_keyword; use self::keywords::get::get_keyword;
use crate::services::keywords::llm_keyword::llm_keyword; use self::keywords::get_website::get_website_keyword;
use crate::services::keywords::on::on_keyword; use self::keywords::last::last_keyword;
use crate::services::keywords::print::print_keyword; use self::keywords::llm_keyword::llm_keyword;
use crate::services::keywords::set::set_keyword; use self::keywords::on::on_keyword;
use crate::services::keywords::set_schedule::set_schedule_keyword; use self::keywords::print::print_keyword;
use crate::services::keywords::wait::wait_keyword; use self::keywords::set::set_keyword;
use crate::services::state::AppState; use self::keywords::set_schedule::set_schedule_keyword;
use self::keywords::wait::wait_keyword;
use crate::shared::AppState;
use log::info; use log::info;
use rhai::{Dynamic, Engine, EvalAltResult}; use rhai::{Dynamic, Engine, EvalAltResult};

View file

@ -1,3 +1,5 @@
pub mod channels;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::Utc; use chrono::Utc;
use livekit::{DataPacketKind, Room, RoomOptions}; use livekit::{DataPacketKind, Room, RoomOptions};
@ -6,7 +8,7 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use crate::services::shared::{BotResponse, UserMessage}; use crate::shared::{BotResponse, UserMessage};
#[async_trait] #[async_trait]
pub trait ChannelAdapter: Send + Sync { pub trait ChannelAdapter: Send + Sync {

View file

@ -1,4 +1,4 @@
use crate::services::{config::EmailConfig, state::AppState}; use crate::{config::EmailConfig, state::AppState};
use log::info; use log::info;
use actix_web::error::ErrorInternalServerError; use actix_web::error::ErrorInternalServerError;

View file

@ -1,14 +1,12 @@
use actix_web::web;
use actix_web::{ web};
use actix_multipart::Multipart; use actix_multipart::Multipart;
use actix_web::{post, HttpResponse}; use actix_web::{post, HttpResponse};
use minio::s3::builders::ObjectContent; use minio::s3::builders::ObjectContent;
use minio::s3::types::ToStream;
use minio::s3::Client; use minio::s3::Client;
use std::io::Write; use std::io::Write;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use minio::s3::types::ToStream;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use minio::s3::client::{Client as MinioClient, ClientBuilder as MinioClientBuilder}; use minio::s3::client::{Client as MinioClient, ClientBuilder as MinioClientBuilder};
@ -16,18 +14,18 @@ use minio::s3::creds::StaticProvider;
use minio::s3::http::BaseUrl; use minio::s3::http::BaseUrl;
use std::str::FromStr; use std::str::FromStr;
use crate::services::config::{AppConfig}; use crate::config::AppConfig;
use crate::services::state::AppState; use crate::state::AppState;
pub async fn init_minio(config: &AppConfig) -> Result<MinioClient, minio::s3::error::Error> { pub async fn init_minio(config: &AppConfig) -> Result<MinioClient, minio::s3::error::Error> {
let scheme = if config.minio.use_ssl { "https" } else { "http" }; let scheme = if config.minio.use_ssl {
"https"
} else {
"http"
};
let base_url = format!("{}://{}", scheme, config.minio.server); let base_url = format!("{}://{}", scheme, config.minio.server);
let base_url = BaseUrl::from_str(&base_url)?; let base_url = BaseUrl::from_str(&base_url)?;
let credentials = StaticProvider::new( let credentials = StaticProvider::new(&config.minio.access_key, &config.minio.secret_key, None);
&config.minio.access_key,
&config.minio.secret_key,
None,
);
let minio_client = MinioClientBuilder::new(base_url) let minio_client = MinioClientBuilder::new(base_url)
.provider(Some(credentials)) .provider(Some(credentials))
@ -104,8 +102,6 @@ pub async fn upload_file(
))) )))
} }
#[post("/files/list/{folder_path}")] #[post("/files/list/{folder_path}")]
pub async fn list_file( pub async fn list_file(
folder_path: web::Path<String>, folder_path: web::Path<String>,
@ -132,11 +128,12 @@ pub async fn list_file(
for item in result.contents { for item in result.contents {
file_list.push(item.name); file_list.push(item.name);
} }
}, }
Err(e) => { Err(e) => {
return Err(actix_web::error::ErrorInternalServerError( return Err(actix_web::error::ErrorInternalServerError(format!(
format!("Failed to list files in MinIO: {}", e) "Failed to list files in MinIO: {}",
)); e
)));
} }
} }
} }

View file

@ -18,7 +18,7 @@ use langchain_rust::{
template_fstring, template_fstring,
}; };
use crate::services::{state::AppState, utils::azure_from_config}; use crate::{state::AppState, utils::azure_from_config};
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct ChatRequest { struct ChatRequest {

View file

@ -1,3 +1,7 @@
pub mod llm_generic;
pub mod llm_local;
pub mod llm_provider;
use async_trait::async_trait; use async_trait::async_trait;
use futures::StreamExt; use futures::StreamExt;
use langchain_rust::{ use langchain_rust::{
@ -9,7 +13,7 @@ use serde_json::Value;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::services::tools::ToolManager; use crate::tools::ToolManager;
#[async_trait] #[async_trait]
pub trait LLMProvider: Send + Sync { pub trait LLMProvider: Send + Sync {

View file

@ -1,4 +1,21 @@
use actix_web::middleware::Logger; mod auth;
mod automation;
mod basic;
mod bot;
mod channels;
mod chart;
mod config;
mod context;
mod email;
mod file;
mod llm;
mod org;
mod session;
mod shared;
mod tools;
mod web_automation;
mod whatsapp;
use log::info; use log::info;
use qdrant_client::Qdrant; use qdrant_client::Qdrant;
use std::sync::Arc; use std::sync::Arc;
@ -7,27 +24,21 @@ use actix_web::{web, App, HttpServer};
use dotenv::dotenv; use dotenv::dotenv;
use sqlx::PgPool; use sqlx::PgPool;
use crate::services::auth::AuthService; use crate::auth::AuthService;
use crate::services::automation::AutomationService; use crate::bot::BotOrchestrator;
use crate::services::channels::ChannelAdapter; use crate::config::AppConfig;
use crate::services::config::AppConfig; use crate::email::{
use crate::services::email::{
get_emails, get_latest_email_from, list_emails, save_click, save_draft, send_email, get_emails, get_latest_email_from, list_emails, save_click, save_draft, send_email,
}; };
use crate::services::file::{list_file, upload_file}; use crate::file::{list_file, upload_file};
use crate::services::llm_generic::generic_chat_completions; use crate::llm::llm_generic::generic_chat_completions;
use crate::services::llm_local::{ use crate::llm::llm_local::{
chat_completions_local, embeddings_local, ensure_llama_servers_running, chat_completions_local, embeddings_local, ensure_llama_servers_running,
}; };
use crate::services::orchestrator::BotOrchestrator; use crate::session::SessionManager;
use crate::services::session::SessionManager; use crate::shared::state::AppState;
use crate::services::state::AppState; use crate::tools::{RedisToolExecutor, ToolManager};
use crate::services::tools::{RedisToolExecutor, ToolManager}; use crate::web_automation::{initialize_browser_pool, BrowserPool};
use crate::services::web_automation::{initialize_browser_pool, BrowserPool};
use crate::services::whatsapp::WhatsAppAdapter;
mod models;
mod services;
#[tokio::main(flavor = "multi_thread")] #[tokio::main(flavor = "multi_thread")]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
@ -41,7 +52,7 @@ async fn main() -> std::io::Result<()> {
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 db_custom = PgPool::connect(&db_custom_url).await.unwrap();
let minio_client = services::file::init_minio(&config) let minio_client = init_minio(&config)
.await .await
.expect("Failed to initialize Minio"); .expect("Failed to initialize Minio");
@ -65,7 +76,8 @@ async fn main() -> std::io::Result<()> {
Ok(redis_url_value) => { Ok(redis_url_value) => {
let client = redis::Client::open(redis_url_value.clone()) let client = redis::Client::open(redis_url_value.clone())
.expect("Failed to create Redis client"); .expect("Failed to create Redis client");
let conn = redis::aio::Connection::new(client) let conn = client
.get_connection()
.await .await
.expect("Failed to create Redis connection"); .expect("Failed to create Redis connection");
Some(Arc::new(conn)) Some(Arc::new(conn))
@ -81,28 +93,27 @@ async fn main() -> std::io::Result<()> {
let session_manager = SessionManager::new(db.clone(), redis_conn.clone()); let session_manager = SessionManager::new(db.clone(), redis_conn.clone());
let auth_service = AuthService::new(db.clone(), redis_conn.clone()); let auth_service = AuthService::new(db.clone(), redis_conn.clone());
let llm_provider: Arc<dyn crate::services::llm_local::LLMProvider> = let llm_provider: Arc<dyn crate::llm_local::LLMProvider> = match std::env::var("LLM_PROVIDER")
match std::env::var("LLM_PROVIDER")
.unwrap_or("mock".to_string()) .unwrap_or("mock".to_string())
.as_str() .as_str()
{ {
"openai" => Arc::new(crate::services::llm_local::OpenAIClient::new( "openai" => Arc::new(crate::llm_local::OpenAIClient::new(
std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY required"), std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY required"),
)), )),
"anthropic" => Arc::new(crate::services::llm_local::AnthropicClient::new( "anthropic" => Arc::new(crate::llm_local::AnthropicClient::new(
std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY required"), std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY required"),
)), )),
_ => Arc::new(crate::services::llm_local::MockLLMProvider::new()), _ => Arc::new(crate::llm_local::MockLLMProvider::new()),
}; };
let web_adapter = Arc::new(crate::services::channels::WebChannelAdapter::new()); let web_adapter = Arc::new(crate::channels::WebChannelAdapter::new());
let voice_adapter = Arc::new(crate::services::channels::VoiceAdapter::new( let voice_adapter = Arc::new(crate::channels::VoiceAdapter::new(
std::env::var("LIVEKIT_URL").unwrap_or("ws://localhost:7880".to_string()), std::env::var("LIVEKIT_URL").unwrap_or("ws://localhost:7880".to_string()),
std::env::var("LIVEKIT_API_KEY").unwrap_or("dev".to_string()), std::env::var("LIVEKIT_API_KEY").unwrap_or("dev".to_string()),
std::env::var("LIVEKIT_API_SECRET").unwrap_or("secret".to_string()), std::env::var("LIVEKIT_API_SECRET").unwrap_or("secret".to_string()),
)); ));
let whatsapp_adapter = Arc::new(crate::services::whatsapp::WhatsAppAdapter::new( let whatsapp_adapter = Arc::new(crate::whatsapp::WhatsAppAdapter::new(
std::env::var("META_ACCESS_TOKEN").unwrap_or("".to_string()), std::env::var("META_ACCESS_TOKEN").unwrap_or("".to_string()),
std::env::var("META_PHONE_NUMBER_ID").unwrap_or("".to_string()), std::env::var("META_PHONE_NUMBER_ID").unwrap_or("".to_string()),
std::env::var("META_WEBHOOK_VERIFY_TOKEN").unwrap_or("".to_string()), std::env::var("META_WEBHOOK_VERIFY_TOKEN").unwrap_or("".to_string()),
@ -111,7 +122,7 @@ async fn main() -> std::io::Result<()> {
let tool_executor = Arc::new( let tool_executor = Arc::new(
RedisToolExecutor::new( RedisToolExecutor::new(
redis_url.as_str(), redis_url.as_str(),
web_adapter.clone() as Arc<dyn crate::services::channels::ChannelAdapter>, web_adapter.clone() as Arc<dyn crate::channels::ChannelAdapter>,
db.clone(), db.clone(),
redis_conn.clone(), redis_conn.clone(),
) )
@ -216,8 +227,8 @@ async fn main() -> std::io::Result<()> {
.service(get_emails) .service(get_emails)
.service(list_emails) .service(list_emails)
.service(send_email) .service(send_email)
.service(crate::services::orchestrator::chat_stream) .service(crate::orchestrator::chat_stream)
.service(crate::services::orchestrator::chat) .service(crate::orchestrator::chat)
.service(chat_completions_local) .service(chat_completions_local)
.service(save_draft) .service(save_draft)
.service(generic_chat_completions) .service(generic_chat_completions)
@ -232,6 +243,11 @@ async fn main() -> std::io::Result<()> {
.service(services::orchestrator::get_sessions) .service(services::orchestrator::get_sessions)
.service(services::orchestrator::get_session_history) .service(services::orchestrator::get_session_history)
.service(services::orchestrator::index) .service(services::orchestrator::index)
.service(create_organization)
.service(get_organization)
.service(list_organizations)
.service(update_organization)
.service(delete_organization)
}) })
.bind((config.server.host.clone(), config.server.port))? .bind((config.server.host.clone(), config.server.port))?
.run() .run()

View file

@ -1 +0,0 @@
pub mod automation_model;

View file

@ -1,18 +0,0 @@
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Bot {
pub bot_id: Uuid,
pub name: String,
pub status: BotStatus,
pub config: serde_json::Value,
pub created_at: chrono::DateTime<Utc>,
pub updated_at: chrono::DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)]
#[serde(rename_all = "snake_case")]
#[sqlx(type_name = "bot_status", rename_all = "snake_case")]
pub enum BotStatus {
Active,
Inactive,
Maintenance,
}

View file

@ -1,12 +0,0 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct organization {
pub org_id: Uuid,
pub name: String,
pub slug: String,
pub created_at: DateTime<Utc>,
}

View file

@ -1,14 +1,7 @@
.service(create_organization)
.service(get_organization)
.service(list_organizations)
.service(update_organization)
.service(delete_organization)
use actix_web::{web, HttpResponse, Result}; use actix_web::{web, HttpResponse, Result};
use chrono::{DateTime, Utc}; use chrono::Utc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool}; use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]

View file

@ -1,16 +0,0 @@
pub mod auth;
pub mod automation;
pub mod bot;
pub mod channels;
pub mod chart;
pub mod config;
pub mod context;
pub mod email;
pub mod file;
pub mod keywords;
pub mod llm;
pub mod session;
pub mod shared;
pub mod tools;
pub mod web_automation;
pub mod whatsapp;

View file

@ -1,233 +0,0 @@
// .service(create_bot)
// .service(get_bot)
// .service(list_bots)
// .service(update_bot)
// .service(delete_bot)
// .service(update_bot_status)
// .service(execute_bot_command)
use crate::services::{config::BotConfig, state::AppState};
use actix_web::{
delete, get, post, put,
web::{self, Data, Json, Path},
HttpResponse, Responder, Result,
};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use sqlx::{postgres::PgQueryResult, FromRow, PgPool};
use uuid::Uuid;
// 1. Core Data Structures
// 2. Request/Response DTOs
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateBotRequest {
pub name: String,
pub initial_config: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateBotRequest {
pub name: Option<String>,
pub status: Option<BotStatus>,
pub config: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BotResponse {
pub bot_id: Uuid,
pub name: String,
pub status: BotStatus,
pub created_at: chrono::DateTime<Utc>,
pub updated_at: chrono::DateTime<Utc>,
}
// 3. Helper Functions
impl From<Bot> for BotResponse {
fn from(bot: Bot) -> Self {
BotResponse {
bot_id: bot.bot_id,
name: bot.name,
status: bot.status,
created_at: bot.created_at,
updated_at: bot.updated_at,
}
}
}
async fn find_bot(bot_id: Uuid, pool: &PgPool) -> Result<Bot, sqlx::Error> {
sqlx::query_as::<_, Bot>("SELECT * FROM bots WHERE bot_id = $1")
.bind(bot_id)
.fetch_one(pool)
.await
}
// 4. API Endpoints
#[post("/bots/create")]
pub async fn create_bot(
payload: Json<CreateBotRequest>,
state: Data<AppState>,
) -> Result<impl Responder, actix_web::Error> {
let new_bot = sqlx::query_as::<_, Bot>(
r#"
INSERT INTO bots (name, status, config)
VALUES ($1, 'active', $2)
RETURNING *
"#,
)
.bind(&payload.name)
.bind(&payload.initial_config)
.fetch_one(&state.db)
.await
.map_err(|e| {
log::error!("Failed to create bot: {}", e);
actix_web::error::ErrorInternalServerError("Failed to create bot")
})?;
Ok(HttpResponse::Created().json(BotResponse::from(new_bot)))
}
#[get("/bots/{bot_id}")]
pub async fn get_bot(
path: Path<Uuid>,
state: Data<AppState>,
) -> Result<impl Responder, actix_web::Error> {
let bot_id = path.into_inner();
let bot = find_bot(bot_id, &state.db).await.map_err(|e| match e {
sqlx::Error::RowNotFound => actix_web::error::ErrorNotFound("Bot not found"),
_ => {
log::error!("Failed to fetch bot: {}", e);
actix_web::error::ErrorInternalServerError("Failed to fetch bot")
}
})?;
Ok(HttpResponse::Ok().json(BotResponse::from(bot)))
}
#[get("/bots")]
pub async fn list_bots(state: Data<AppState>) -> Result<impl Responder, actix_web::Error> {
let bots = sqlx::query_as::<_, Bot>("SELECT * FROM bots ORDER BY created_at DESC")
.fetch_all(&state.db)
.await
.map_err(|e| {
log::error!("Failed to list bots: {}", e);
actix_web::error::ErrorInternalServerError("Failed to list bots")
})?;
let responses: Vec<BotResponse> = bots.into_iter().map(BotResponse::from).collect();
Ok(HttpResponse::Ok().json(responses))
}
#[put("/bots/{bot_id}")]
pub async fn update_bot(
path: Path<Uuid>,
payload: Json<UpdateBotRequest>,
state: Data<AppState>,
) -> Result<impl Responder, actix_web::Error> {
let bot_id = path.into_inner();
let updated_bot = sqlx::query_as::<_, Bot>(
r#"
UPDATE bots
SET
name = COALESCE($1, name),
status = COALESCE($2, status),
config = COALESCE($3, config),
updated_at = NOW()
WHERE bot_id = $4
RETURNING *
"#,
)
.bind(&payload.name)
.bind(&payload.status)
.bind(&payload.config)
.bind(bot_id)
.fetch_one(&state.db)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => actix_web::error::ErrorNotFound("Bot not found"),
_ => {
log::error!("Failed to update bot: {}", e);
actix_web::error::ErrorInternalServerError("Failed to update bot")
}
})?;
Ok(HttpResponse::Ok().json(BotResponse::from(updated_bot)))
}
#[delete("/bots/{bot_id}")]
pub async fn delete_bot(
path: Path<Uuid>,
state: Data<AppState>,
) -> Result<impl Responder, actix_web::Error> {
let bot_id = path.into_inner();
let result = sqlx::query("DELETE FROM bots WHERE bot_id = $1")
.bind(bot_id)
.execute(&state.db)
.await
.map_err(|e| {
log::error!("Failed to delete bot: {}", e);
actix_web::error::ErrorInternalServerError("Failed to delete bot")
})?;
if result.rows_affected() == 0 {
return Err(actix_web::error::ErrorNotFound("Bot not found"));
}
Ok(HttpResponse::NoContent().finish())
}
#[put("/bots/{bot_id}/status")]
pub async fn update_bot_status(
path: Path<Uuid>,
new_status: Json<BotStatus>,
state: Data<AppState>,
) -> Result<impl Responder, actix_web::Error> {
let bot_id = path.into_inner();
let updated_bot = sqlx::query_as::<_, Bot>(
"UPDATE bots SET status = $1, updated_at = NOW() WHERE bot_id = $2 RETURNING *",
)
.bind(new_status.into_inner())
.bind(bot_id)
.fetch_one(&state.db)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => actix_web::error::ErrorNotFound("Bot not found"),
_ => {
log::error!("Failed to update bot status: {}", e);
actix_web::error::ErrorInternalServerError("Failed to update bot status")
}
})?;
Ok(HttpResponse::Ok().json(BotResponse::from(updated_bot)))
}
#[post("/bots/{bot_id}/execute")]
pub async fn execute_bot_command(
path: Path<Uuid>,
command: Json<serde_json::Value>,
state: Data<AppState>,
) -> Result<impl Responder, actix_web::Error> {
let bot_id = path.into_inner();
// Verify bot exists
let _ = find_bot(bot_id, &state.db).await.map_err(|e| match e {
sqlx::Error::RowNotFound => actix_web::error::ErrorNotFound("Bot not found"),
_ => {
log::error!("Failed to fetch bot: {}", e);
actix_web::error::ErrorInternalServerError("Failed to fetch bot")
}
})?;
// Here you would implement your bot execution logic
// For now, we'll just echo back the command
Ok(HttpResponse::Ok().json(json!({
"bot_id": bot_id,
"command": command,
"result": "Command executed successfully (simulated)"
})))
}

View file

@ -1,215 +0,0 @@
use actix_web::{web, HttpResponse, Result};
use serde::{Deserialize, Serialize};
#[derive(serde::Deserialize)]
struct ChatRequest {
input: String,
context: Option<AppContext>,
}
#[derive(serde::Deserialize)]
struct AppContext {
view_type: Option<String>,
email_context: Option<EmailContext>,
}
#[derive(serde::Deserialize)]
struct EmailContext {
id: String,
subject: String,
labels: Vec<String>,
from: Option<String>,
to: Option<Vec<String>>,
body: Option<String>,
}
#[derive(serde::Serialize)]
struct ChatResponse {
response: String,
tool_calls: Option<Vec<ToolCall>>,
}
#[derive(serde::Serialize)]
struct ToolCall {
tool_name: String,
parameters: serde_json::Value,
}
#[derive(serde::Serialize)]
struct ToolDefinition {
name: String,
description: String,
parameters: serde_json::Value,
}
#[actix_web::post("/chat")]
pub async fn chat(
web::Json(request): web::Json<ChatRequest>,
state: web::Data<AppState>,
) -> Result<HttpResponse> {
let azure_config = from_config(&state.config.clone().unwrap().ai);
let open_ai = OpenAI::new(azure_config);
// Define available tools based on context
let tools = get_available_tools(&request.context);
// Build the prompt with context and available tools
let system_prompt = build_system_prompt(&request.context, &tools);
let user_message = format!("{}\n\nUser input: {}", system_prompt, request.input);
let response = match open_ai.invoke(&user_message).await {
Ok(res) => res,
Err(err) => {
error!("Error invoking API: {}", err);
return Err(actix_web::error::ErrorInternalServerError(
"Failed to invoke OpenAI API",
));
}
};
// Parse the response for tool calls
let tool_calls = parse_tool_calls(&response);
let chat_response = ChatResponse {
response,
tool_calls,
};
Ok(HttpResponse::Ok().json(chat_response))
}
fn get_available_tools(context: &Option<AppContext>) -> Vec<ToolDefinition> {
let mut tools = Vec::new();
if let Some(ctx) = context {
if let Some(view_type) = &ctx.view_type {
match view_type.as_str() {
"email" => {
tools.push(ToolDefinition {
name: "replyEmail".to_string(),
description: "Reply to the current email with generated content".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The reply content to send"
}
},
"required": ["content"]
}),
});
tools.push(ToolDefinition {
name: "forwardEmail".to_string(),
description: "Forward the current email to specified recipients".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"recipients": {
"type": "array",
"items": {"type": "string"},
"description": "Email addresses to forward to"
},
"content": {
"type": "string",
"description": "Additional message to include"
}
},
"required": ["recipients"]
}),
});
}
_ => {}
}
}
}
tools
}
fn build_system_prompt(context: &Option<AppContext>, tools: &[ToolDefinition]) -> String {
let mut prompt = String::new();
if let Some(ctx) = context {
if let Some(view_type) = &ctx.view_type {
match view_type.as_str() {
"email" => {
if let Some(email_ctx) = &ctx.email_context {
prompt.push_str(&format!(
"You are an email assistant. Current email context:\n\
Subject: {}\n\
ID: {}\n\
Labels: {:?}\n\n",
email_ctx.subject, email_ctx.id, email_ctx.labels
));
if let Some(from) = &email_ctx.from {
prompt.push_str(&format!("From: {}\n", from));
}
if let Some(body) = &email_ctx.body {
prompt.push_str(&format!("Body: {}\n", body));
}
}
}
_ => {}
}
}
}
if !tools.is_empty() {
prompt.push_str("\nAvailable tools:\n");
for tool in tools {
prompt.push_str(&format!(
"- {}: {}\n Parameters: {}\n\n",
tool.name, tool.description, tool.parameters
));
}
prompt.push_str(
"If you need to use a tool, respond with:\n\
TOOL_CALL: tool_name\n\
PARAMETERS: {json_parameters}\n\
RESPONSE: your_response_text\n\n\
Otherwise, just provide a normal response.\n"
);
}
prompt
}
fn parse_tool_calls(response: &str) -> Option<Vec<ToolCall>> {
if !response.contains("TOOL_CALL:") {
return None;
}
let mut tool_calls = Vec::new();
let lines: Vec<&str> = response.lines().collect();
let mut i = 0;
while i < lines.len() {
if lines[i].starts_with("TOOL_CALL:") {
let tool_name = lines[i].replace("TOOL_CALL:", "").trim().to_string();
// Look for parameters in the next line
if i + 1 < lines.len() && lines[i + 1].starts_with("PARAMETERS:") {
let params_str = lines[i + 1].replace("PARAMETERS:", "").trim();
if let Ok(parameters) = serde_json::from_str::<serde_json::Value>(params_str) {
tool_calls.push(ToolCall {
tool_name,
parameters,
});
}
}
}
i += 1;
}
if tool_calls.is_empty() {
None
} else {
Some(tool_calls)
}
}

View file

@ -1,4 +1,4 @@
use crate::services::shared::shared::UserSession; use crate::shared::shared::UserSession;
use sqlx::Row; use sqlx::Row;
use redis::{aio::Connection as ConnectionManager, AsyncCommands}; use redis::{aio::Connection as ConnectionManager, AsyncCommands};

Some files were not shown because too many files have changed in this diff Show more