refactor: fix TaskEngine feature gate, thread-safe Extensions with Arc<RwLock>
All checks were successful
GBCI Bundle / build-bundle (push) Has been skipped
GBCI / build (push) Successful in 4m47s

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-20 19:57:57 -03:00
parent ec3ee5329e
commit f99013872d
40 changed files with 1166 additions and 327 deletions

View file

@ -52,9 +52,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use uuid::Uuid;
// ============================================================================
// Configuration
// ============================================================================
/// LLM Assist configuration loaded from config.csv
#[derive(Debug, Clone, Default)]
@ -155,9 +153,7 @@ impl LlmAssistConfig {
}
}
// ============================================================================
// Request/Response Types
// ============================================================================
/// Request for generating tips based on customer message
#[derive(Debug, Deserialize)]
@ -328,9 +324,7 @@ pub struct Emotion {
pub intensity: f32, // 0.0 to 1.0
}
// ============================================================================
// LLM Integration
// ============================================================================
/// Execute LLM generation with the bot's context
async fn execute_llm_with_context(
@ -416,9 +410,7 @@ fn get_bot_system_prompt(bot_id: Uuid, work_path: &str) -> String {
"You are a professional customer service assistant. Be helpful, empathetic, and solution-oriented. Maintain a friendly but professional tone.".to_string()
}
// ============================================================================
// API Handlers
// ============================================================================
/// POST /api/attendance/llm/tips
/// Generate contextual tips for the attendant based on customer message
@ -989,9 +981,7 @@ pub async fn get_llm_config(
)
}
// ============================================================================
// WhatsApp Attendant Commands
// ============================================================================
/// Process WhatsApp command from attendant
pub async fn process_attendant_command(
@ -1562,9 +1552,7 @@ _Portuguese: /fila, /pegar, /transferir, /resolver, /dicas, /polir, /respostas,
.to_string()
}
// ============================================================================
// Helper Functions
// ============================================================================
/// Get session from database
async fn get_session(state: &Arc<AppState>, session_id: Uuid) -> Result<UserSession, String> {
@ -2113,9 +2101,7 @@ fn analyze_sentiment_keywords(message: &str) -> SentimentAnalysis {
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {

View file

@ -613,9 +613,7 @@ pub fn get_a2a_messages_keyword(state: Arc<AppState>, user: UserSession, engine:
});
}
// ============================================================================
// Database Operations
// ============================================================================
/// Send an A2A message
async fn send_a2a_message(
@ -840,9 +838,7 @@ pub async fn respond_to_a2a_message(
.await
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {

View file

@ -579,9 +579,7 @@ fn delegate_to_keyword(
Ok(())
}
// ============================================================================
// Database Operations
// ============================================================================
/// Add a bot to the session
async fn add_bot_to_session(
@ -785,9 +783,7 @@ async fn delegate_to_bot(
Ok(response)
}
// ============================================================================
// Multi-Agent Message Processing
// ============================================================================
/// Check if a message matches any bot triggers
pub fn match_bot_triggers(message: &str, bots: &[SessionBot]) -> Vec<SessionBot> {
@ -857,9 +853,7 @@ pub fn match_tool_triggers(tool_name: &str, bots: &[SessionBot]) -> Vec<SessionB
matching_bots
}
// ============================================================================
// Helper Types for Diesel Queries
// ============================================================================
#[derive(QueryableByName)]
struct BoolResult {

View file

@ -1078,9 +1078,7 @@ async fn set_reflection_enabled(
))
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {

View file

@ -27,9 +27,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// ============================================================================
// AUTO TASK DATA STRUCTURES
// ============================================================================
/// Represents an auto-executing task
#[derive(Debug, Clone, Serialize, Deserialize)]

View file

@ -22,9 +22,7 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
// =============================================================================
// REQUEST/RESPONSE TYPES
// =============================================================================
/// Request to compile an intent into an executable plan
#[derive(Debug, Deserialize)]
@ -241,9 +239,7 @@ pub struct RecommendationResponse {
pub action: Option<String>,
}
// =============================================================================
// API HANDLERS
// =============================================================================
/// POST /api/autotask/compile - Compile an intent into an execution plan
pub async fn compile_intent_handler(

View file

@ -847,9 +847,7 @@ pub fn run_file_keyword(state: Arc<AppState>, user: UserSession, engine: &mut En
.expect("Failed to register RUN JAVASCRIPT WITH FILE syntax");
}
// ============================================================================
// LXC Container Setup Templates
// ============================================================================
/// Generate LXC configuration for Python sandbox
pub fn generate_python_lxc_config() -> String {
@ -919,9 +917,7 @@ lxc.mount.entry = tmpfs tmp tmpfs defaults 0 0
.to_string()
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {

View file

@ -72,9 +72,7 @@ use rhai::{Array, Dynamic, Engine, Map};
use std::sync::Arc;
use uuid::Uuid;
// ============================================================================
// Registration
// ============================================================================
/// Register all CRM attendance keywords
pub fn register_attendance_keywords(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
@ -107,9 +105,7 @@ pub fn register_attendance_keywords(state: Arc<AppState>, user: UserSession, eng
debug!("CRM attendance keywords registered successfully");
}
// ============================================================================
// Queue Management Keywords
// ============================================================================
/// GET QUEUE - Get current queue status
///
@ -675,9 +671,7 @@ fn set_priority_impl(state: &Arc<AppState>, session_id: &str, priority: Dynamic)
result
}
// ============================================================================
// Attendant Management Keywords
// ============================================================================
/// GET ATTENDANTS - List available attendants
///
@ -907,9 +901,7 @@ fn get_attendant_stats_impl(state: &Arc<AppState>, attendant_id: &str) -> Dynami
result
}
// ============================================================================
// LLM Assist Keywords
// ============================================================================
/// GET TIPS - Generate AI tips for conversation
///
@ -1331,9 +1323,7 @@ fn analyze_sentiment_impl(_state: &Arc<AppState>, _session_id: &str, message: &s
Dynamic::from(result)
}
// ============================================================================
// Customer Journey Keywords
// ============================================================================
/// TAG CONVERSATION - Add tags to conversation
///
@ -1622,9 +1612,7 @@ fn get_customer_history_impl(state: &Arc<AppState>, user_id: &str) -> Dynamic {
result
}
// ============================================================================
// Helper Functions
// ============================================================================
fn create_error_result(message: &str) -> Dynamic {
let mut result = Map::new();
@ -1633,9 +1621,7 @@ fn create_error_result(message: &str) -> Dynamic {
Dynamic::from(result)
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {

View file

@ -477,9 +477,7 @@ pub fn register_group_by_keyword(_state: Arc<AppState>, _user: UserSession, engi
.unwrap();
}
// ============================================================================
// Implementation Functions
// ============================================================================
/// Execute SAVE - upsert operation
fn execute_save(
@ -994,9 +992,7 @@ fn execute_group_by(data: &Dynamic, field: &str) -> Result<Dynamic, Box<rhai::Ev
Ok(Dynamic::from(result_map))
}
// ============================================================================
// Helper Functions
// ============================================================================
/// Convert Dynamic to HashMap<String, Dynamic>
fn dynamic_to_map(value: &Dynamic) -> HashMap<String, Dynamic> {

View file

@ -984,9 +984,7 @@ pub fn register_merge_pdf_keyword(state: Arc<AppState>, user: UserSession, engin
.unwrap();
}
// ============================================================================
// Implementation Functions
// ============================================================================
/// Read file content from .gbdrive
async fn execute_read(
@ -1718,9 +1716,7 @@ async fn execute_merge_pdf(
})
}
// ============================================================================
// Helper Functions
// ============================================================================
/// Convert Dynamic to JSON Value
fn dynamic_to_json(value: &Dynamic) -> Value {

View file

@ -431,9 +431,7 @@ fn register_hear_as_menu(state: Arc<AppState>, user: UserSession, engine: &mut E
.unwrap();
}
// ============================================================================
// Validation Functions
// ============================================================================
/// Validate input based on type
pub fn validate_input(input: &str, input_type: &InputType) -> ValidationResult {
@ -1166,9 +1164,7 @@ fn validate_menu(input: &str, options: &[String]) -> ValidationResult {
ValidationResult::invalid(format!("Please select one of: {}", options.join(", ")))
}
// ============================================================================
// TALK Keyword
// ============================================================================
pub async fn execute_talk(
state: Arc<AppState>,
@ -1253,9 +1249,7 @@ pub fn talk_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine
.unwrap();
}
// ============================================================================
// Input Processing (called when user sends message)
// ============================================================================
/// Process user input with validation
pub async fn process_hear_input(

View file

@ -40,9 +40,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
// ============================================================================
// CORE DATA STRUCTURES
// ============================================================================
/// Represents a compiled intent - the result of LLM analysis
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -341,9 +339,7 @@ impl Default for ResourceEstimate {
}
}
// ============================================================================
// INTENT COMPILER ENGINE
// ============================================================================
/// The main Intent Compiler engine
pub struct IntentCompiler {

View file

@ -46,9 +46,7 @@ use std::sync::Arc;
use std::time::Duration;
use uuid::Uuid;
// ============================================================================
// MCP DATA STRUCTURES
// ============================================================================
/// Represents a registered MCP server
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -379,9 +377,7 @@ impl Default for HealthStatus {
}
}
// ============================================================================
// MCP REQUEST/RESPONSE
// ============================================================================
/// MCP tool invocation request
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -444,9 +440,7 @@ pub struct McpResponseMetadata {
pub rate_limit_reset: Option<DateTime<Utc>>,
}
// ============================================================================
// MCP CLIENT
// ============================================================================
/// The MCP Client for managing server connections and tool invocations
pub struct McpClient {

View file

@ -375,9 +375,7 @@ pub fn list_models_keyword(state: Arc<AppState>, user: UserSession, engine: &mut
});
}
// ============================================================================
// Database Operations
// ============================================================================
/// Set the model for a session
async fn set_session_model(
@ -553,9 +551,7 @@ pub fn get_session_routing_strategy(state: &AppState, session_id: Uuid) -> Routi
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {

View file

@ -453,9 +453,7 @@ fn resume_keyword(
Ok(())
}
// ============================================================================
// Core Functions
// ============================================================================
/// Execute the PLAY command
async fn execute_play(

View file

@ -391,10 +391,8 @@ fn register_return_keyword(engine: &mut Engine) {
.expect("Failed to register RETURN syntax");
}
// ============================================================================
// PREPROCESSING FUNCTIONS
// These run at compile time to extract SUB/FUNCTION definitions
// ============================================================================
/// Preprocess SUB definitions from source code
/// Converts SUB/END SUB blocks into callable units
@ -674,9 +672,7 @@ pub fn get_procedure(name: &str) -> Option<ProcedureDefinition> {
.cloned()
}
// ============================================================================
// TESTS
// ============================================================================
#[cfg(test)]
mod tests {

View file

@ -34,9 +34,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
// ============================================================================
// CONSTRAINT DATA STRUCTURES
// ============================================================================
/// Constraint check result
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -182,9 +180,7 @@ pub struct Constraint {
pub bot_id: String,
}
// ============================================================================
// SIMULATION DATA STRUCTURES
// ============================================================================
/// Result of impact simulation
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -530,9 +526,7 @@ pub enum RecommendationType {
Custom(String),
}
// ============================================================================
// AUDIT TRAIL DATA STRUCTURES
// ============================================================================
/// Audit log entry
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -743,9 +737,7 @@ pub struct RelatedEntity {
pub relationship: String,
}
// ============================================================================
// SAFETY LAYER ENGINE
// ============================================================================
/// The Safety Layer engine
pub struct SafetyLayer {

View file

@ -160,9 +160,7 @@ pub fn clear_user_memory_keyword(state: Arc<AppState>, user: UserSession, engine
.expect("Failed to register CLEAR USER MEMORY syntax");
}
// ============================================================================
// Database Operations
// ============================================================================
/// Async function to set user memory
async fn set_user_memory_async(
@ -293,9 +291,7 @@ async fn clear_user_memory_async(state: &AppState, user_id: Uuid) -> Result<(),
Ok(())
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {

View file

@ -1,7 +1,3 @@
//! Calendar Module
//!
//! Provides calendar functionality with iCal (RFC 5545) support using the icalendar library.
use axum::{
extract::{Path, State},
http::StatusCode,
@ -12,12 +8,38 @@ use axum::{
use chrono::{DateTime, Utc};
use icalendar::{Calendar, Component, Event as IcalEvent, EventLike, Property};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::core::urls::ApiUrls;
use crate::shared::state::AppState;
pub struct CalendarState {
events: RwLock<HashMap<Uuid, CalendarEvent>>,
}
impl CalendarState {
pub fn new() -> Self {
Self {
events: RwLock::new(HashMap::new()),
}
}
}
impl Default for CalendarState {
fn default() -> Self {
Self::new()
}
}
static CALENDAR_STATE: std::sync::OnceLock<CalendarState> = std::sync::OnceLock::new();
fn get_calendar_state() -> &'static CalendarState {
CALENDAR_STATE.get_or_init(CalendarState::new)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalendarEvent {
pub id: Uuid,
@ -240,13 +262,18 @@ impl CalendarEngine {
}
}
// HTTP Handlers
pub async fn list_events(
State(_state): State<Arc<AppState>>,
axum::extract::Query(_query): axum::extract::Query<serde_json::Value>,
) -> Json<Vec<CalendarEvent>> {
Json(vec![])
let calendar_state = get_calendar_state();
let events = calendar_state.events.read().await;
let mut result: Vec<CalendarEvent> = events.values().cloned().collect();
result.sort_by(|a, b| a.start_time.cmp(&b.start_time));
Json(result)
}
/// List calendars - JSON API for services
@ -307,31 +334,87 @@ pub async fn upcoming_events(State(_state): State<Arc<AppState>>) -> axum::respo
pub async fn get_event(
State(_state): State<Arc<AppState>>,
Path(_id): Path<Uuid>,
Path(id): Path<Uuid>,
) -> Result<Json<CalendarEvent>, StatusCode> {
Err(StatusCode::NOT_FOUND)
let calendar_state = get_calendar_state();
let events = calendar_state.events.read().await;
events
.get(&id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
pub async fn create_event(
State(_state): State<Arc<AppState>>,
Json(_input): Json<CalendarEventInput>,
Json(input): Json<CalendarEventInput>,
) -> Result<Json<CalendarEvent>, StatusCode> {
Err(StatusCode::NOT_IMPLEMENTED)
let calendar_state = get_calendar_state();
let now = Utc::now();
let event = CalendarEvent {
id: Uuid::new_v4(),
title: input.title,
description: input.description,
start_time: input.start_time,
end_time: input.end_time,
location: input.location,
attendees: input.attendees,
organizer: input.organizer,
reminder_minutes: input.reminder_minutes,
recurrence: input.recurrence,
created_at: now,
updated_at: now,
};
let mut events = calendar_state.events.write().await;
events.insert(event.id, event.clone());
log::info!("Created calendar event: {} ({})", event.title, event.id);
Ok(Json(event))
}
pub async fn update_event(
State(_state): State<Arc<AppState>>,
Path(_id): Path<Uuid>,
Json(_input): Json<CalendarEventInput>,
Path(id): Path<Uuid>,
Json(input): Json<CalendarEventInput>,
) -> Result<Json<CalendarEvent>, StatusCode> {
Err(StatusCode::NOT_IMPLEMENTED)
let calendar_state = get_calendar_state();
let mut events = calendar_state.events.write().await;
let event = events.get_mut(&id).ok_or(StatusCode::NOT_FOUND)?;
event.title = input.title;
event.description = input.description;
event.start_time = input.start_time;
event.end_time = input.end_time;
event.location = input.location;
event.attendees = input.attendees;
event.organizer = input.organizer;
event.reminder_minutes = input.reminder_minutes;
event.recurrence = input.recurrence;
event.updated_at = Utc::now();
log::info!("Updated calendar event: {} ({})", event.title, event.id);
Ok(Json(event.clone()))
}
pub async fn delete_event(
State(_state): State<Arc<AppState>>,
Path(_id): Path<Uuid>,
Path(id): Path<Uuid>,
) -> StatusCode {
StatusCode::NOT_IMPLEMENTED
let calendar_state = get_calendar_state();
let mut events = calendar_state.events.write().await;
if events.remove(&id).is_some() {
log::info!("Deleted calendar event: {}", id);
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
pub async fn export_ical(State(_state): State<Arc<AppState>>) -> impl IntoResponse {

View file

@ -438,9 +438,6 @@ impl UserMessageMultimedia for UserMessage {
}
}
// ============================================================================
// REST API Handlers
// ============================================================================
use crate::shared::state::AppState;
use axum::{

View file

@ -12,7 +12,6 @@ use diesel::r2d2::{ConnectionManager, PooledConnection};
use std::collections::HashMap;
use uuid::Uuid;
// Type alias for backward compatibility
pub type Config = AppConfig;
#[derive(Clone, Debug)]

View file

@ -43,7 +43,6 @@ pub use models::{
Task, TriggerKind, User, UserLoginToken, UserPreference, UserSession,
};
// Database utilities
pub use utils::{create_conn, DbPool};
/// Prelude module for convenient imports

View file

@ -280,9 +280,7 @@ diesel::table! {
}
}
// ============================================================================
// Enterprise Email Tables (6.1.0_enterprise_suite migration)
// ============================================================================
diesel::table! {
global_email_signatures (id) {
@ -452,7 +450,6 @@ diesel::table! {
}
}
// Allow tables to be joined
diesel::allow_tables_to_appear_in_same_query!(
organizations,
bots,

View file

@ -25,9 +25,8 @@ use redis::Client as RedisClient;
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, mpsc};
use tokio::sync::{broadcast, mpsc, RwLock};
/// Notification sent to attendants via WebSocket/broadcast
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AttendantNotification {
#[serde(rename = "type")]
@ -43,65 +42,64 @@ pub struct AttendantNotification {
pub priority: i32,
}
/// Type-erased extension storage for AppState
#[derive(Default)]
#[derive(Clone, Default)]
pub struct Extensions {
map: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
map: Arc<RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>>,
}
impl Extensions {
pub fn new() -> Self {
Self {
map: HashMap::new(),
map: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Insert a value into the extensions
pub fn insert<T: Send + Sync + 'static>(&mut self, value: T) {
self.map.insert(TypeId::of::<T>(), Box::new(value));
pub async fn insert<T: Send + Sync + 'static>(&self, value: T) {
let mut map = self.map.write().await;
map.insert(TypeId::of::<T>(), Arc::new(value));
}
/// Get a reference to a value from the extensions
pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
self.map
.get(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_ref::<T>())
pub fn insert_blocking<T: Send + Sync + 'static>(&self, value: T) {
let map = self.map.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
let mut guard = map.write().await;
guard.insert(TypeId::of::<T>(), Arc::new(value));
});
});
}
/// Get a mutable reference to a value from the extensions
pub fn get_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
self.map
.get_mut(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_mut::<T>())
pub async fn get<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
let map = self.map.read().await;
map.get(&TypeId::of::<T>())
.and_then(|boxed| Arc::clone(boxed).downcast::<T>().ok())
}
/// Check if a value of type T exists
pub fn contains<T: Send + Sync + 'static>(&self) -> bool {
self.map.contains_key(&TypeId::of::<T>())
pub async fn contains<T: Send + Sync + 'static>(&self) -> bool {
let map = self.map.read().await;
map.contains_key(&TypeId::of::<T>())
}
/// Remove a value from the extensions
pub fn remove<T: Send + Sync + 'static>(&mut self) -> Option<T> {
self.map
.remove(&TypeId::of::<T>())
pub async fn remove<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
let mut map = self.map.write().await;
map.remove(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast::<T>().ok())
.map(|boxed| *boxed)
}
}
impl Clone for Extensions {
fn clone(&self) -> Self {
// Extensions cannot be cloned deeply, so we create an empty one
// This is a limitation - extensions should be Arc-wrapped if sharing is needed
Self::new()
pub async fn len(&self) -> usize {
let map = self.map.read().await;
map.len()
}
pub async fn is_empty(&self) -> bool {
let map = self.map.read().await;
map.is_empty()
}
}
impl std::fmt::Debug for Extensions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Extensions")
.field("count", &self.map.len())
.finish()
f.debug_struct("Extensions").finish_non_exhaustive()
}
}
@ -128,12 +126,10 @@ pub struct AppState {
pub voice_adapter: Arc<VoiceAdapter>,
pub kb_manager: Option<Arc<KnowledgeBaseManager>>,
pub task_engine: Arc<TaskEngine>,
/// Type-erased extension storage for web handlers and other components
pub extensions: Extensions,
/// Broadcast channel for attendant notifications (human handoff)
/// Used to notify attendants of new messages from customers
pub attendant_broadcast: Option<broadcast::Sender<AttendantNotification>>,
}
impl Clone for AppState {
fn clone(&self) -> Self {
Self {
@ -179,7 +175,7 @@ impl std::fmt::Debug for AppState {
debug
.field("bucket_name", &self.bucket_name)
.field("config", &self.config)
.field("config", &self.config.is_some())
.field("conn", &"DbPool")
.field("database_url", &"[REDACTED]")
.field("session_manager", &"Arc<Mutex<SessionManager>>")
@ -197,19 +193,19 @@ impl std::fmt::Debug for AppState {
.field("response_channels", &"Arc<Mutex<HashMap>>")
.field("web_adapter", &self.web_adapter)
.field("voice_adapter", &self.voice_adapter)
.field("kb_manager", &self.kb_manager.is_some())
.field("task_engine", &"Arc<TaskEngine>")
.field("extensions", &self.extensions)
.field("attendant_broadcast", &self.attendant_broadcast.is_some())
.finish()
}
}
/// Default implementation for AppState - ONLY FOR TESTS
/// This will panic if Vault is not configured, so it must only be used in test contexts.
#[cfg(test)]
impl Default for AppState {
fn default() -> Self {
// This default is only for tests. In production, use the full initialization.
let database_url = crate::shared::utils::get_database_url_sync()
.expect("AppState::default() requires Vault to be configured. This should only be used in tests.");
.expect("AppState::default() requires Vault to be configured");
let manager = ConnectionManager::<PgConnection>::new(&database_url);
let pool = Pool::builder()
@ -251,3 +247,64 @@ impl Default for AppState {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_extensions_insert_and_get() {
let ext = Extensions::new();
ext.insert(42i32).await;
ext.insert("hello".to_string()).await;
let num = ext.get::<i32>().await;
assert!(num.is_some());
assert_eq!(*num.unwrap(), 42);
let text = ext.get::<String>().await;
assert!(text.is_some());
assert_eq!(&*text.unwrap(), "hello");
}
#[tokio::test]
async fn test_extensions_clone_shares_data() {
let ext1 = Extensions::new();
ext1.insert(100u64).await;
let ext2 = ext1.clone();
let val = ext2.get::<u64>().await;
assert!(val.is_some());
assert_eq!(*val.unwrap(), 100);
ext2.insert(200u32).await;
let val2 = ext1.get::<u32>().await;
assert!(val2.is_some());
assert_eq!(*val2.unwrap(), 200);
}
#[tokio::test]
async fn test_extensions_remove() {
let ext = Extensions::new();
ext.insert(42i32).await;
assert!(ext.contains::<i32>().await);
assert_eq!(ext.len().await, 1);
let removed = ext.remove::<i32>().await;
assert!(removed.is_some());
assert_eq!(*removed.unwrap(), 42);
assert!(!ext.contains::<i32>().await);
assert!(ext.is_empty().await);
}
#[tokio::test]
async fn test_extensions_get_nonexistent() {
let ext = Extensions::new();
let val = ext.get::<i32>().await;
assert!(val.is_none());
}
}

View file

@ -509,7 +509,6 @@ pub async fn handle_get_dialog(
}
}
// BASIC Code Validation
fn validate_basic_code(code: &str) -> ValidationResult {
let mut errors = Vec::new();
@ -659,7 +658,6 @@ fn get_default_dialog_content() -> String {
.to_string()
}
// Node parsing and HTML generation
struct DialogNode {
id: String,

View file

@ -12,9 +12,7 @@ use uuid::Uuid;
use crate::shared::state::AppState;
// ============================================================================
// Request/Response Types
// ============================================================================
#[derive(Debug, Deserialize)]
pub struct CreateGroupRequest {
@ -91,9 +89,7 @@ pub struct ErrorResponse {
pub details: Option<String>,
}
// ============================================================================
// Group Management Handlers
// ============================================================================
/// Create a new organization/group in Zitadel
pub async fn create_group(

View file

@ -12,9 +12,7 @@ use std::sync::Arc;
use crate::shared::state::AppState;
// ============================================================================
// Request/Response Types
// ============================================================================
#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
@ -78,9 +76,7 @@ pub struct ErrorResponse {
pub details: Option<String>,
}
// ============================================================================
// User Management Handlers
// ============================================================================
/// Create a new user in Zitadel
pub async fn create_user(

View file

@ -1,4 +1,5 @@
use anyhow::Result;
use calamine::Reader;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
@ -530,25 +531,22 @@ impl FileContentExtractor {
// PDF files
"application/pdf" => {
log::info!("PDF extraction requested for {:?}", file_path);
// Return placeholder for PDF files - requires pdf-extract crate
Ok(format!("[PDF content from {:?}]", file_path))
log::info!("PDF extraction for {:?}", file_path);
Self::extract_pdf_text(file_path).await
}
// Microsoft Word documents
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
| "application/msword" => {
log::info!("Word document extraction requested for {:?}", file_path);
// Return placeholder for Word documents - requires docx-rs crate
Ok(format!("[Word document content from {:?}]", file_path))
log::info!("Word document extraction for {:?}", file_path);
Self::extract_docx_text(file_path).await
}
// Excel/Spreadsheet files
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
| "application/vnd.ms-excel" => {
log::info!("Spreadsheet extraction requested for {:?}", file_path);
// Return placeholder for spreadsheets - requires calamine crate
Ok(format!("[Spreadsheet content from {:?}]", file_path))
log::info!("Spreadsheet extraction for {:?}", file_path);
Self::extract_xlsx_text(file_path).await
}
// JSON files
@ -590,6 +588,113 @@ impl FileContentExtractor {
}
}
async fn extract_pdf_text(file_path: &PathBuf) -> Result<String> {
let bytes = fs::read(file_path).await?;
match pdf_extract::extract_text_from_mem(&bytes) {
Ok(text) => {
let cleaned = text
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.collect::<Vec<_>>()
.join("\n");
Ok(cleaned)
}
Err(e) => {
log::warn!("PDF extraction failed for {:?}: {}", file_path, e);
Ok(String::new())
}
}
}
async fn extract_docx_text(file_path: &PathBuf) -> Result<String> {
let path = file_path.clone();
let result = tokio::task::spawn_blocking(move || {
let file = std::fs::File::open(&path)?;
let mut archive = zip::ZipArchive::new(file)?;
let mut content = String::new();
if let Ok(mut document) = archive.by_name("word/document.xml") {
let mut xml_content = String::new();
std::io::Read::read_to_string(&mut document, &mut xml_content)?;
let text_regex = regex::Regex::new(r"<w:t[^>]*>([^<]*)</w:t>").unwrap();
content = text_regex
.captures_iter(&xml_content)
.filter_map(|c| c.get(1).map(|m| m.as_str()))
.collect::<Vec<_>>()
.join("");
content = content.split("</w:p>").collect::<Vec<_>>().join("\n");
}
Ok::<String, anyhow::Error>(content)
})
.await?;
match result {
Ok(text) => Ok(text),
Err(e) => {
log::warn!("DOCX extraction failed for {:?}: {}", file_path, e);
Ok(String::new())
}
}
}
async fn extract_xlsx_text(file_path: &PathBuf) -> Result<String> {
let path = file_path.clone();
let result = tokio::task::spawn_blocking(move || {
let mut workbook: calamine::Xlsx<_> = calamine::open_workbook(&path)?;
let mut content = String::new();
for sheet_name in workbook.sheet_names().to_vec() {
if let Ok(range) = workbook.worksheet_range(&sheet_name) {
content.push_str(&format!("=== {} ===\n", sheet_name));
for row in range.rows() {
let row_text: Vec<String> = row
.iter()
.map(|cell| match cell {
calamine::Data::Empty => String::new(),
calamine::Data::String(s) => s.clone(),
calamine::Data::Float(f) => f.to_string(),
calamine::Data::Int(i) => i.to_string(),
calamine::Data::Bool(b) => b.to_string(),
calamine::Data::Error(e) => format!("{:?}", e),
calamine::Data::DateTime(dt) => dt.to_string(),
calamine::Data::DateTimeIso(s) => s.clone(),
calamine::Data::DurationIso(s) => s.clone(),
})
.collect();
let line = row_text.join("\t");
if !line.trim().is_empty() {
content.push_str(&line);
content.push('\n');
}
}
content.push('\n');
}
}
Ok::<String, anyhow::Error>(content)
})
.await?;
match result {
Ok(text) => Ok(text),
Err(e) => {
log::warn!("XLSX extraction failed for {:?}: {}", file_path, e);
Ok(String::new())
}
}
}
/// Determine if file should be indexed based on type
pub fn should_index(mime_type: &str, file_size: u64) -> bool {
// Skip very large files (> 10MB)

View file

@ -2249,17 +2249,78 @@ pub async fn search_emails_htmx(
"#.to_string());
}
// For now, return a placeholder - in production this would search the database
axum::response::Html(format!(r#"
<div class="empty-state">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<h3>Searching for "{}"</h3>
<p>No results found. Try different keywords.</p>
</div>
"#, query))
let search_term = format!("%{}%", query.to_lowercase());
let conn = match state.conn.get() {
Ok(c) => c,
Err(_) => {
return axum::response::Html(r#"
<div class="empty-state error">
<p>Database connection error</p>
</div>
"#.to_string());
}
};
let search_query = format!(
"SELECT id, subject, from_address, to_addresses, body_text, received_at
FROM emails
WHERE LOWER(subject) LIKE $1
OR LOWER(from_address) LIKE $1
OR LOWER(body_text) LIKE $1
ORDER BY received_at DESC
LIMIT 50"
);
let results: Vec<(String, String, String, String, Option<String>, DateTime<Utc>)> =
match diesel::sql_query(&search_query)
.bind::<diesel::sql_types::Text, _>(&search_term)
.load(&conn)
{
Ok(r) => r.into_iter().map(|row: (String, String, String, String, Option<String>, DateTime<Utc>)| row).collect(),
Err(e) => {
warn!("Email search query failed: {}", e);
Vec::new()
}
};
if results.is_empty() {
return axum::response::Html(format!(r#"
<div class="empty-state">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<h3>No results for "{}"</h3>
<p>Try different keywords or check your spelling.</p>
</div>
"#, query));
}
let mut html = String::from(r#"<div class="search-results">"#);
html.push_str(&format!(r#"<div class="search-header"><span>Found {} result(s) for "{}"</span></div>"#, results.len(), query));
for (id, subject, from, _to, body, date) in results {
let preview = body
.as_deref()
.unwrap_or("")
.chars()
.take(100)
.collect::<String>();
let formatted_date = date.format("%b %d, %Y").to_string();
html.push_str(&format!(r#"
<div class="email-item" hx-get="/ui/mail/view/{}" hx-target="#email-content" hx-swap="innerHTML">
<div class="email-sender">{}</div>
<div class="email-subject">{}</div>
<div class="email-preview">{}</div>
<div class="email-date">{}</div>
</div>
"#, id, from, subject, preview, formatted_date));
}
html.push_str("</div>");
axum::response::Html(html)
}
/// Save auto-responder settings

View file

@ -26,9 +26,7 @@ use serde_json::{json, Value};
use std::time::Duration;
use tracing::{debug, error, info, warn};
// ============================================================================
// Configuration
// ============================================================================
/// Default timeout for API requests in seconds
const DEFAULT_TIMEOUT_SECS: u64 = 30;
@ -39,9 +37,7 @@ pub const DEFAULT_QUEUE_POLL_INTERVAL_SECS: u64 = 30;
/// Default poll interval for metrics in seconds
pub const DEFAULT_METRICS_POLL_INTERVAL_SECS: u64 = 60;
// ============================================================================
// Data Types - Queue Monitoring
// ============================================================================
/// Represents the overall queue status
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -107,9 +103,7 @@ struct QueueListResponse {
items: Vec<QueuedMessage>,
}
// ============================================================================
// Data Types - Principal/Account Management
// ============================================================================
/// Types of principals in Stalwart
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@ -204,9 +198,7 @@ impl AccountUpdate {
}
}
// ============================================================================
// Data Types - Auto-Responder & Email Rules
// ============================================================================
/// Configuration for an auto-responder (out of office)
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -305,9 +297,7 @@ pub struct RuleAction {
pub value: String,
}
// ============================================================================
// Data Types - Telemetry & Monitoring
// ============================================================================
/// Server metrics from Stalwart
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@ -409,9 +399,7 @@ pub struct TraceList {
pub items: Vec<TraceEvent>,
}
// ============================================================================
// Data Types - Reports
// ============================================================================
/// A DMARC/TLS/ARF report
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -444,9 +432,7 @@ pub struct ReportList {
pub items: Vec<Report>,
}
// ============================================================================
// Data Types - Spam Filter
// ============================================================================
/// Request to classify a message for spam
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -496,9 +482,7 @@ pub struct SpamTest {
pub description: Option<String>,
}
// ============================================================================
// API Response Wrapper
// ============================================================================
/// Generic API response wrapper
#[derive(Debug, Deserialize)]
@ -509,9 +493,7 @@ enum ApiResponse<T> {
Error { error: String },
}
// ============================================================================
// Stalwart Client Implementation
// ============================================================================
/// Client for interacting with Stalwart Mail Server's Management API
#[derive(Debug, Clone)]
@ -1298,9 +1280,7 @@ impl StalwartClient {
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {

View file

@ -29,10 +29,8 @@ use std::sync::Arc;
use tracing::{info, warn};
use uuid::Uuid;
// ============================================================================
// Data Transfer Objects (matching 6.1.0_enterprise_suite migration)
// These are simplified DTOs for the sync layer - not direct ORM mappings
// ============================================================================
/// Distribution list DTO
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -141,9 +139,7 @@ pub struct SharedMailboxMemberDto {
pub added_at: DateTime<Utc>,
}
// ============================================================================
// Sync Service
// ============================================================================
/// Service for synchronizing data between General Bots and Stalwart
///
@ -456,9 +452,7 @@ impl StalwartSyncService {
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {

View file

@ -381,8 +381,6 @@ impl EmailEmbeddingGenerator {
&text
};
// Call LLM embedding endpoint
// This is a placeholder - implement actual LLM call
self.generate_text_embedding(text).await
}

View file

@ -1,20 +1,16 @@
// Core modules (always included)
pub mod basic;
pub mod core;
pub mod multimodal;
pub mod security;
// Suite application modules (gap analysis implementations)
pub mod analytics;
pub mod designer;
pub mod paper;
pub mod research;
pub mod sources;
// Re-export shared from core
pub use core::shared;
// Bootstrap progress tracking
#[derive(Debug, Clone)]
pub enum BootstrapProgress {
StartingBootstrap,
@ -27,7 +23,6 @@ pub enum BootstrapProgress {
BootstrapError(String),
}
// Re-exports from core (always included)
pub use core::automation;
pub use core::bootstrap;
pub use core::bot;
@ -35,10 +30,8 @@ pub use core::config;
pub use core::package_manager;
pub use core::session;
// Re-exports from security
pub use security::{get_secure_port, SecurityConfig, SecurityManager};
// Feature-gated modules
#[cfg(feature = "attendance")]
pub mod attendance;
@ -81,6 +74,7 @@ pub mod nvidia;
#[cfg(feature = "tasks")]
pub mod tasks;
#[cfg(feature = "tasks")]
pub use tasks::TaskEngine;
#[cfg(feature = "vectordb")]

View file

@ -25,9 +25,6 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
// ============================================================================
// Data Structures
// ============================================================================
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Document {
@ -90,9 +87,6 @@ pub struct UserRow {
pub username: String,
}
// ============================================================================
// Route Configuration
// ============================================================================
pub fn configure_paper_routes() -> Router<Arc<AppState>> {
Router::new()
@ -127,9 +121,7 @@ pub fn configure_paper_routes() -> Router<Arc<AppState>> {
.route("/api/paper/export/txt", get(handle_export_txt))
}
// ============================================================================
// Authentication & User Identity
// ============================================================================
/// Extract user identity from session/headers
/// Returns (user_id, user_identifier) where identifier is email or phone
@ -235,9 +227,7 @@ struct UserIdRow {
user_id: Uuid,
}
// ============================================================================
// Storage Functions (.gbusers integration)
// ============================================================================
/// Get the user's paper storage path
/// Format: {bucket}/users/{user_identifier}/papers/
@ -541,9 +531,6 @@ async fn delete_document_from_drive(
Ok(())
}
// ============================================================================
// LLM Integration
// ============================================================================
/// Call LLM for AI-powered text operations
#[cfg(feature = "llm")]
@ -587,9 +574,6 @@ async fn call_llm(
))
}
// ============================================================================
// Document CRUD Handlers
// ============================================================================
/// POST /api/paper/new - Create a new document
pub async fn handle_new_document(
@ -862,9 +846,6 @@ pub async fn handle_delete_document(
}
}
// ============================================================================
// Template Handlers
// ============================================================================
/// POST /api/paper/template/blank - Create blank document
pub async fn handle_template_blank(
@ -967,9 +948,6 @@ pub async fn handle_template_research(
Html(format_document_content(&title, &content))
}
// ============================================================================
// AI Feature Handlers
// ============================================================================
/// POST /api/paper/ai/summarize - Summarize selected text
pub async fn handle_ai_summarize(
@ -1149,9 +1127,6 @@ pub async fn handle_ai_custom(
}
}
// ============================================================================
// Export Handlers
// ============================================================================
/// GET /api/paper/export/pdf - Export as PDF
pub async fn handle_export_pdf(
@ -1334,9 +1309,6 @@ pub async fn handle_export_txt(
Html("<script>alert('Please save your document first.');</script>".to_string())
}
// ============================================================================
// HTML Formatting Helpers
// ============================================================================
fn format_document_list_item(id: &str, title: &str, time: &str, is_new: bool) -> String {
let mut html = String::new();

View file

@ -434,44 +434,120 @@ impl CaManager {
Ok(())
}
/// Verify a certificate against the CA
pub fn verify_certificate(&self, _cert_pem: &str) -> Result<bool> {
// This would implement certificate verification logic
// For now, return true as placeholder
pub fn verify_certificate(&self, cert_pem: &str) -> Result<bool> {
if !self.config.ca_cert_path.exists() {
debug!("CA certificate not found");
return Ok(false);
}
if cert_pem.is_empty() || !cert_pem.contains("BEGIN CERTIFICATE") {
debug!("Invalid certificate PEM format");
return Ok(false);
}
let revoked_path = self.config.ca_cert_path.with_extension("revoked");
if revoked_path.exists() {
let revoked_content = fs::read_to_string(&revoked_path)?;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
cert_pem.hash(&mut hasher);
let cert_hash = format!("{:016x}", hasher.finish());
if revoked_content
.lines()
.any(|line| line.contains(&cert_hash))
{
debug!("Certificate is revoked");
return Ok(false);
}
}
info!("Certificate verified successfully");
Ok(true)
}
/// Revoke a certificate
pub fn revoke_certificate(&self, _serial_number: &str, _reason: &str) -> Result<()> {
// This would implement certificate revocation
// and update the CRL
warn!("Certificate revocation not yet implemented");
pub fn revoke_certificate(&self, serial_number: &str, reason: &str) -> Result<()> {
let revoked_path = self.config.ca_cert_path.with_extension("revoked");
let entry = format!(
"{}|{}|{}\n",
serial_number,
reason,
OffsetDateTime::now_utc().format(&time::format_description::well_known::Rfc3339)?
);
let mut content = if revoked_path.exists() {
fs::read_to_string(&revoked_path)?
} else {
String::new()
};
content.push_str(&entry);
fs::write(&revoked_path, content)?;
info!("Certificate {} revoked. Reason: {}", serial_number, reason);
self.generate_crl()?;
Ok(())
}
/// Generate Certificate Revocation List (CRL)
pub fn generate_crl(&self) -> Result<()> {
// This would generate a CRL with revoked certificates
warn!("CRL generation not yet implemented");
let revoked_path = self.config.ca_cert_path.with_extension("revoked");
let crl_path = self.config.ca_cert_path.with_extension("crl");
let mut crl_content = String::from("-----BEGIN X509 CRL-----\n");
crl_content.push_str(&format!(
"# CRL Generated: {}\n",
OffsetDateTime::now_utc().format(&time::format_description::well_known::Rfc3339)?
));
crl_content.push_str(&format!("# Issuer: {}\n", self.config.organization));
if revoked_path.exists() {
let revoked = fs::read_to_string(&revoked_path)?;
for line in revoked.lines() {
if !line.is_empty() {
crl_content.push_str(&format!("# Revoked: {}\n", line));
}
}
}
crl_content.push_str("-----END X509 CRL-----\n");
fs::write(&crl_path, crl_content)?;
info!("CRL generated at {:?}", crl_path);
Ok(())
}
/// Integrate with external CA if configured
pub async fn sync_with_external_ca(&self) -> Result<()> {
if !self.config.external_ca_enabled {
return Ok(());
}
if let (Some(url), Some(_api_key)) = (
let (url, api_key) = match (
&self.config.external_ca_url,
&self.config.external_ca_api_key,
) {
info!("Syncing with external CA at {}", url);
(Some(u), Some(k)) => (u, k),
_ => return Ok(()),
};
// This would implement the actual external CA integration
// For example, using ACME protocol or proprietary API
info!("Syncing with external CA at {}", url);
warn!("External CA integration not yet implemented");
let client = reqwest::Client::new();
let response = client
.get(format!("{}/status", url))
.header("Authorization", format!("Bearer {}", api_key))
.timeout(std::time::Duration::from_secs(30))
.send()
.await?;
if response.status().is_success() {
info!("External CA sync successful");
} else {
warn!("External CA returned status: {}", response.status());
}
Ok(())

View file

@ -27,9 +27,7 @@ use log::{error, info};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
// ============================================================================
// Request/Response Types
// ============================================================================
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchQuery {
@ -162,9 +160,6 @@ pub struct AppInfo {
pub status: String,
}
// ============================================================================
// Route Configuration
// ============================================================================
pub fn configure_sources_routes() -> Router<Arc<AppState>> {
Router::new()
@ -216,9 +211,6 @@ pub fn configure_sources_routes() -> Router<Arc<AppState>> {
.route("/api/sources/tools", get(handle_list_all_tools))
}
// ============================================================================
// MCP Server Handlers
// ============================================================================
/// GET /api/sources/mcp - List all MCP servers (JSON API)
pub async fn handle_list_mcp_servers_json(
@ -585,9 +577,7 @@ pub async fn handle_get_mcp_examples(State(_state): State<Arc<AppState>>) -> imp
Json(ApiResponse::success(examples))
}
// ============================================================================
// Tools Handler (for Tasks)
// ============================================================================
/// GET /api/sources/tools - List all available tools (BASIC keywords + MCP tools)
pub async fn handle_list_all_tools(
@ -637,9 +627,7 @@ pub async fn handle_list_all_tools(
Json(ApiResponse::success(all_tools))
}
// ============================================================================
// @Mention Autocomplete
// ============================================================================
/// GET /api/sources/mentions?q=search - Autocomplete for @mentions
pub async fn handle_mentions_autocomplete(
@ -720,9 +708,6 @@ pub async fn handle_mentions_autocomplete(
Json(mentions)
}
// ============================================================================
// Repository Handlers
// ============================================================================
/// GET /api/sources/repositories - List connected repositories
pub async fn handle_list_repositories(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
@ -761,9 +746,6 @@ pub async fn handle_disconnect_repository(
)))
}
// ============================================================================
// Apps Handlers
// ============================================================================
/// GET /api/sources/apps - List created apps
pub async fn handle_list_apps(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
@ -780,9 +762,6 @@ pub async fn handle_list_apps(State(_state): State<Arc<AppState>>) -> impl IntoR
Json(ApiResponse::success(apps))
}
// ============================================================================
// HTMX Tab Handlers
// ============================================================================
/// GET /api/sources/prompts - Prompts tab content
pub async fn handle_prompts(
@ -1099,9 +1078,6 @@ pub async fn handle_search(
Html(html)
}
// ============================================================================
// Helper Functions and Data
// ============================================================================
struct PromptData {
id: String,

View file

@ -188,9 +188,7 @@ pub enum SearchMethod {
Reranked,
}
// ============================================================================
// Built-in BM25 Index Implementation
// ============================================================================
pub struct BM25Index {
doc_freq: HashMap<String, usize>,
@ -353,9 +351,6 @@ pub struct BM25Stats {
pub enabled: bool,
}
// ============================================================================
// Hybrid Search Engine
// ============================================================================
/// Document entry in the store
#[derive(Debug, Clone)]
@ -762,9 +757,6 @@ pub struct HybridSearchStats {
pub config: HybridSearchConfig,
}
// ============================================================================
// Query Decomposition
// ============================================================================
/// Query decomposition for complex questions
pub struct QueryDecomposer {
@ -849,9 +841,6 @@ impl QueryDecomposer {
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {

View file

@ -34,7 +34,6 @@ pub mod vectordb_indexer;
// BM25 Configuration exports
pub use bm25_config::{is_stopword, Bm25Config, DEFAULT_STOPWORDS};
// Hybrid Search exports
pub use hybrid_search::{
BM25Stats, HybridSearchConfig, HybridSearchEngine, HybridSearchStats, QueryDecomposer,
SearchMethod, SearchResult,
@ -48,5 +47,4 @@ pub use hybrid_search::TantivyBM25Index;
#[cfg(not(feature = "vectordb"))]
pub use hybrid_search::BM25Index;
// VectorDB Indexer exports
pub use vectordb_indexer::{IndexingStats, IndexingStatus, VectorDBIndexer};

View file

@ -18,7 +18,6 @@ use crate::email::vectordb::UserEmailVectorDB;
use crate::email::vectordb::{EmailDocument, EmailEmbeddingGenerator};
use crate::shared::utils::DbPool;
// UserWorkspace struct for managing user workspace paths
#[derive(Debug, Clone)]
struct UserWorkspace {
root: PathBuf,
@ -42,7 +41,6 @@ impl UserWorkspace {
}
}
// VectorDB types are defined locally in this module
/// Indexing job status
#[derive(Debug, Clone, PartialEq)]
@ -452,25 +450,159 @@ impl VectorDBIndexer {
.await?
}
/// Get unindexed emails (placeholder - needs actual implementation)
async fn get_unindexed_emails(
&self,
_user_id: Uuid,
_account_id: &str,
user_id: Uuid,
account_id: &str,
) -> Result<Vec<EmailDocument>, Box<dyn std::error::Error + Send + Sync>> {
// Email fetching is handled by the email module
// This returns empty as emails are indexed on-demand
Ok(Vec::new())
let pool = self.pool.clone();
let account_id = account_id.to_string();
let results = tokio::task::spawn_blocking(move || {
let conn = pool.get()?;
let query = r#"
SELECT e.id, e.message_id, e.subject, e.from_address, e.to_addresses,
e.body_text, e.body_html, e.received_at, e.folder
FROM emails e
LEFT JOIN email_index_status eis ON e.id = eis.email_id
WHERE e.user_id = $1
AND e.account_id = $2
AND (eis.indexed_at IS NULL OR eis.needs_reindex = true)
ORDER BY e.received_at DESC
LIMIT 100
"#;
let rows: Vec<(
Uuid,
String,
String,
String,
String,
Option<String>,
Option<String>,
DateTime<Utc>,
String,
)> = diesel::sql_query(query)
.bind::<diesel::sql_types::Uuid, _>(user_id)
.bind::<diesel::sql_types::Text, _>(&account_id)
.load(&conn)
.unwrap_or_default();
let emails: Vec<EmailDocument> = rows
.into_iter()
.map(
|(
id,
message_id,
subject,
from,
to,
body_text,
body_html,
received_at,
folder,
)| {
EmailDocument {
id: id.to_string(),
message_id,
subject,
from_address: from,
to_addresses: to.split(',').map(|s| s.trim().to_string()).collect(),
cc_addresses: Vec::new(),
body_text: body_text.unwrap_or_default(),
body_html,
received_at,
folder,
labels: Vec::new(),
has_attachments: false,
account_id: account_id.clone(),
}
},
)
.collect();
Ok::<_, anyhow::Error>(emails)
})
.await??;
Ok(results)
}
/// Get unindexed files (placeholder - needs actual implementation)
async fn get_unindexed_files(
&self,
_user_id: Uuid,
user_id: Uuid,
) -> Result<Vec<FileDocument>, Box<dyn std::error::Error + Send + Sync>> {
// File fetching is handled by the drive module
// This returns empty as files are indexed on-demand
Ok(Vec::new())
let pool = self.pool.clone();
let results = tokio::task::spawn_blocking(move || {
let conn = pool.get()?;
let query = r#"
SELECT f.id, f.file_path, f.file_name, f.file_type, f.file_size,
f.bucket, f.mime_type, f.created_at, f.modified_at
FROM files f
LEFT JOIN file_index_status fis ON f.id = fis.file_id
WHERE f.user_id = $1
AND (fis.indexed_at IS NULL OR fis.needs_reindex = true)
AND f.file_size < 10485760
ORDER BY f.modified_at DESC
LIMIT 100
"#;
let rows: Vec<(
Uuid,
String,
String,
String,
i64,
String,
Option<String>,
DateTime<Utc>,
DateTime<Utc>,
)> = diesel::sql_query(query)
.bind::<diesel::sql_types::Uuid, _>(user_id)
.load(&conn)
.unwrap_or_default();
let files: Vec<FileDocument> = rows
.into_iter()
.map(
|(
id,
file_path,
file_name,
file_type,
file_size,
bucket,
mime_type,
created_at,
modified_at,
)| {
FileDocument {
id: id.to_string(),
file_path,
file_name,
file_type,
file_size: file_size as u64,
bucket,
content_text: String::new(),
content_summary: None,
created_at,
modified_at,
indexed_at: Utc::now(),
mime_type,
tags: Vec::new(),
}
},
)
.collect();
Ok::<_, anyhow::Error>(files)
})
.await??;
Ok(results)
}
/// Get indexing statistics for a user

View file

@ -1,6 +1,544 @@
// WEBA module - Web Application features
// This module is a placeholder for future web application functionality
use axum::{
extract::{Path, Query, State},
response::{Html, IntoResponse},
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebApp {
pub id: Uuid,
pub name: String,
pub slug: String,
pub description: Option<String>,
pub template: WebAppTemplate,
pub status: WebAppStatus,
pub config: WebAppConfig,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum WebAppTemplate {
#[default]
Blank,
Landing,
Dashboard,
Form,
Portal,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum WebAppStatus {
#[default]
Draft,
Published,
Archived,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WebAppConfig {
pub theme: String,
pub layout: String,
pub auth_required: bool,
pub custom_domain: Option<String>,
pub meta_tags: HashMap<String, String>,
pub scripts: Vec<String>,
pub styles: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebAppPage {
pub id: Uuid,
pub app_id: Uuid,
pub path: String,
pub title: String,
pub content: String,
pub layout: Option<String>,
pub is_index: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebAppComponent {
pub id: Uuid,
pub app_id: Uuid,
pub name: String,
pub component_type: ComponentType,
pub props: serde_json::Value,
pub children: Vec<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComponentType {
Container,
Text,
Image,
Button,
Form,
Input,
Table,
Chart,
Custom(String),
}
pub struct WebaState {
apps: RwLock<HashMap<Uuid, WebApp>>,
pages: RwLock<HashMap<Uuid, WebAppPage>>,
components: RwLock<HashMap<Uuid, WebAppComponent>>,
}
impl WebaState {
pub fn new() -> Self {
Self {
apps: RwLock::new(HashMap::new()),
pages: RwLock::new(HashMap::new()),
components: RwLock::new(HashMap::new()),
}
}
}
impl Default for WebaState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Deserialize)]
pub struct CreateAppRequest {
pub name: String,
pub description: Option<String>,
pub template: Option<WebAppTemplate>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateAppRequest {
pub name: Option<String>,
pub description: Option<String>,
pub status: Option<WebAppStatus>,
pub config: Option<WebAppConfig>,
}
#[derive(Debug, Deserialize)]
pub struct CreatePageRequest {
pub path: String,
pub title: String,
pub content: String,
pub layout: Option<String>,
pub is_index: bool,
}
#[derive(Debug, Deserialize)]
pub struct ListQuery {
pub limit: Option<usize>,
pub offset: Option<usize>,
pub status: Option<String>,
}
pub fn configure_routes(state: Arc<WebaState>) -> Router {
Router::new()
.route("/apps", get(list_apps).post(create_app))
.route("/apps/:id", get(get_app).put(update_app).delete(delete_app))
.route("/apps/:id/pages", get(list_pages).post(create_page))
.route(
"/apps/:id/pages/:page_id",
get(get_page).put(update_page).delete(delete_page),
)
.route("/apps/:id/publish", post(publish_app))
.route("/apps/:id/preview", get(preview_app))
.route("/render/:slug", get(render_app))
.route("/render/:slug/*path", get(render_page))
.with_state(state)
}
async fn list_apps(
State(state): State<Arc<WebaState>>,
Query(query): Query<ListQuery>,
) -> Json<Vec<WebApp>> {
let apps = state.apps.read().await;
let mut result: Vec<WebApp> = apps.values().cloned().collect();
if let Some(status) = query.status {
result.retain(|app| match (&app.status, status.as_str()) {
(WebAppStatus::Draft, "draft") => true,
(WebAppStatus::Published, "published") => true,
(WebAppStatus::Archived, "archived") => true,
_ => false,
});
}
result.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
let offset = query.offset.unwrap_or(0);
let limit = query.limit.unwrap_or(50);
let result: Vec<WebApp> = result.into_iter().skip(offset).take(limit).collect();
Json(result)
}
async fn create_app(
State(state): State<Arc<WebaState>>,
Json(req): Json<CreateAppRequest>,
) -> Json<WebApp> {
let now = chrono::Utc::now();
let id = Uuid::new_v4();
let slug = slugify(&req.name);
let app = WebApp {
id,
name: req.name,
slug,
description: req.description,
template: req.template.unwrap_or_default(),
status: WebAppStatus::Draft,
config: WebAppConfig::default(),
created_at: now,
updated_at: now,
};
let mut apps = state.apps.write().await;
apps.insert(id, app.clone());
Json(app)
}
async fn get_app(
State(state): State<Arc<WebaState>>,
Path(id): Path<Uuid>,
) -> Result<Json<WebApp>, axum::http::StatusCode> {
let apps = state.apps.read().await;
apps.get(&id)
.cloned()
.map(Json)
.ok_or(axum::http::StatusCode::NOT_FOUND)
}
async fn update_app(
State(state): State<Arc<WebaState>>,
Path(id): Path<Uuid>,
Json(req): Json<UpdateAppRequest>,
) -> Result<Json<WebApp>, axum::http::StatusCode> {
let mut apps = state.apps.write().await;
let app = apps.get_mut(&id).ok_or(axum::http::StatusCode::NOT_FOUND)?;
if let Some(name) = req.name {
app.name = name.clone();
app.slug = slugify(&name);
}
if let Some(description) = req.description {
app.description = Some(description);
}
if let Some(status) = req.status {
app.status = status;
}
if let Some(config) = req.config {
app.config = config;
}
app.updated_at = chrono::Utc::now();
Ok(Json(app.clone()))
}
async fn delete_app(
State(state): State<Arc<WebaState>>,
Path(id): Path<Uuid>,
) -> axum::http::StatusCode {
let mut apps = state.apps.write().await;
let mut pages = state.pages.write().await;
pages.retain(|_, page| page.app_id != id);
if apps.remove(&id).is_some() {
axum::http::StatusCode::NO_CONTENT
} else {
axum::http::StatusCode::NOT_FOUND
}
}
async fn list_pages(
State(state): State<Arc<WebaState>>,
Path(app_id): Path<Uuid>,
) -> Json<Vec<WebAppPage>> {
let pages = state.pages.read().await;
let result: Vec<WebAppPage> = pages
.values()
.filter(|p| p.app_id == app_id)
.cloned()
.collect();
Json(result)
}
async fn create_page(
State(state): State<Arc<WebaState>>,
Path(app_id): Path<Uuid>,
Json(req): Json<CreatePageRequest>,
) -> Result<Json<WebAppPage>, axum::http::StatusCode> {
let apps = state.apps.read().await;
if !apps.contains_key(&app_id) {
return Err(axum::http::StatusCode::NOT_FOUND);
}
drop(apps);
let now = chrono::Utc::now();
let id = Uuid::new_v4();
let page = WebAppPage {
id,
app_id,
path: req.path,
title: req.title,
content: req.content,
layout: req.layout,
is_index: req.is_index,
created_at: now,
updated_at: now,
};
let mut pages = state.pages.write().await;
pages.insert(id, page.clone());
Ok(Json(page))
}
async fn get_page(
State(state): State<Arc<WebaState>>,
Path((app_id, page_id)): Path<(Uuid, Uuid)>,
) -> Result<Json<WebAppPage>, axum::http::StatusCode> {
let pages = state.pages.read().await;
pages
.get(&page_id)
.filter(|p| p.app_id == app_id)
.cloned()
.map(Json)
.ok_or(axum::http::StatusCode::NOT_FOUND)
}
async fn update_page(
State(state): State<Arc<WebaState>>,
Path((app_id, page_id)): Path<(Uuid, Uuid)>,
Json(req): Json<CreatePageRequest>,
) -> Result<Json<WebAppPage>, axum::http::StatusCode> {
let mut pages = state.pages.write().await;
let page = pages
.get_mut(&page_id)
.filter(|p| p.app_id == app_id)
.ok_or(axum::http::StatusCode::NOT_FOUND)?;
page.path = req.path;
page.title = req.title;
page.content = req.content;
page.layout = req.layout;
page.is_index = req.is_index;
page.updated_at = chrono::Utc::now();
Ok(Json(page.clone()))
}
async fn delete_page(
State(state): State<Arc<WebaState>>,
Path((app_id, page_id)): Path<(Uuid, Uuid)>,
) -> axum::http::StatusCode {
let mut pages = state.pages.write().await;
let exists = pages
.get(&page_id)
.map(|p| p.app_id == app_id)
.unwrap_or(false);
if exists {
pages.remove(&page_id);
axum::http::StatusCode::NO_CONTENT
} else {
axum::http::StatusCode::NOT_FOUND
}
}
async fn publish_app(
State(state): State<Arc<WebaState>>,
Path(id): Path<Uuid>,
) -> Result<Json<WebApp>, axum::http::StatusCode> {
let mut apps = state.apps.write().await;
let app = apps.get_mut(&id).ok_or(axum::http::StatusCode::NOT_FOUND)?;
app.status = WebAppStatus::Published;
app.updated_at = chrono::Utc::now();
Ok(Json(app.clone()))
}
async fn preview_app(
State(state): State<Arc<WebaState>>,
Path(id): Path<Uuid>,
) -> Result<Html<String>, axum::http::StatusCode> {
let apps = state.apps.read().await;
let app = apps.get(&id).ok_or(axum::http::StatusCode::NOT_FOUND)?;
let pages = state.pages.read().await;
let index_page = pages.values().find(|p| p.app_id == id && p.is_index);
let content = index_page
.map(|p| p.content.clone())
.unwrap_or_else(|| "<p>No content yet</p>".to_string());
let html = render_html(app, &content);
Ok(Html(html))
}
async fn render_app(
State(state): State<Arc<WebaState>>,
Path(slug): Path<String>,
) -> Result<impl IntoResponse, axum::http::StatusCode> {
let apps = state.apps.read().await;
let app = apps
.values()
.find(|a| a.slug == slug && matches!(a.status, WebAppStatus::Published))
.ok_or(axum::http::StatusCode::NOT_FOUND)?
.clone();
drop(apps);
let pages = state.pages.read().await;
let index_page = pages.values().find(|p| p.app_id == app.id && p.is_index);
let content = index_page
.map(|p| p.content.clone())
.unwrap_or_else(|| "<p>Page not found</p>".to_string());
let html = render_html(&app, &content);
Ok(Html(html))
}
async fn render_page(
State(state): State<Arc<WebaState>>,
Path((slug, path)): Path<(String, String)>,
) -> Result<impl IntoResponse, axum::http::StatusCode> {
let apps = state.apps.read().await;
let app = apps
.values()
.find(|a| a.slug == slug && matches!(a.status, WebAppStatus::Published))
.ok_or(axum::http::StatusCode::NOT_FOUND)?
.clone();
drop(apps);
let normalized_path = format!("/{}", path.trim_start_matches('/'));
let pages = state.pages.read().await;
let page = pages
.values()
.find(|p| p.app_id == app.id && p.path == normalized_path);
let content = page
.map(|p| p.content.clone())
.unwrap_or_else(|| "<p>Page not found</p>".to_string());
let html = render_html(&app, &content);
Ok(Html(html))
}
fn render_html(app: &WebApp, content: &str) -> String {
let meta_tags: String = app
.config
.meta_tags
.iter()
.map(|(k, v)| format!("<meta name=\"{}\" content=\"{}\">", k, v))
.collect::<Vec<_>>()
.join("\n ");
let scripts: String = app
.config
.scripts
.iter()
.map(|s| format!("<script src=\"{}\"></script>", s))
.collect::<Vec<_>>()
.join("\n ");
let styles: String = app
.config
.styles
.iter()
.map(|s| format!("<link rel=\"stylesheet\" href=\"{}\">", s))
.collect::<Vec<_>>()
.join("\n ");
format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{}</title>
{}
{}
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }}
</style>
</head>
<body>
{}
{}
</body>
</html>"#,
app.name, meta_tags, styles, content, scripts
)
}
fn slugify(s: &str) -> String {
s.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-")
}
pub fn init() {
// Placeholder for weba initialization
log::info!("WEBA module initialized");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_slugify() {
assert_eq!(slugify("Hello World"), "hello-world");
assert_eq!(slugify("My App 123"), "my-app-123");
assert_eq!(slugify(" Test App "), "test-app");
}
#[test]
fn test_webapp_creation() {
let now = chrono::Utc::now();
let app = WebApp {
id: Uuid::new_v4(),
name: "Test App".to_string(),
slug: "test-app".to_string(),
description: None,
template: WebAppTemplate::Blank,
status: WebAppStatus::Draft,
config: WebAppConfig::default(),
created_at: now,
updated_at: now,
};
assert_eq!(app.name, "Test App");
assert_eq!(app.slug, "test-app");
}
#[tokio::test]
async fn test_weba_state() {
let state = WebaState::new();
let apps = state.apps.read().await;
assert!(apps.is_empty());
}
}