pub mod db;
pub mod entities;
pub mod llama;
pub mod mcp;
#[cfg(test)]
mod tests;
use crate::llama::LlamaClient;
use dotenvy::dotenv;
use sea_orm::DatabaseConnection;
use std::env;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tauri::menu::{Menu, MenuItem};
use tauri::tray::{TrayIconBuilder, TrayIconEvent};
use tauri::Manager;
use sea_orm::{ConnectionTrait, Statement, DatabaseBackend};
use std::sync::atomic::AtomicUsize;
use tauri::Emitter;
pub struct AppState {
pub db: DatabaseConnection,
pub llama: Arc<LlamaClient>,
pub mcp_tx: tokio::sync::broadcast::Sender<serde_json::Value>,
pub connection_count: Arc<AtomicUsize>,
pub app_handle: tauri::AppHandle,
}
// --- Path Resolution Helpers ---
pub fn find_build_assets_dir_logic(res_dir: Option<PathBuf>, exe_dir: Option<PathBuf>) -> Option<PathBuf> {
// 1. Check Resource Dir (Release / Installed) - prioritizes nested structure
if let Some(res_dir) = res_dir {
let candidate = res_dir.join("build_assets");
if candidate.exists() {
log::info!("Found build_assets in resource_dir: {:?}", candidate);
return Some(candidate);
}
}
// 2. Check relative to Executable (Debug / Dev) - walks up directory tree
if let Some(exe_dir) = exe_dir {
let mut p = exe_dir;
for _ in 0..10 { // Increased depth to check up to 9 levels up
let candidate = p.join("build_assets");
if candidate.exists() {
log::info!("Found build_assets relative to exe: {:?}", candidate);
return Some(candidate);
}
if !p.pop() { break; }
}
}
None
}
pub fn find_build_assets_dir(app_handle: &tauri::AppHandle) -> Option<PathBuf> {
let res_dir = app_handle.path().resource_dir().ok();
let exe_dir = env::current_exe().ok().and_then(|p| p.parent().map(|p| p.to_path_buf()));
find_build_assets_dir_logic(res_dir, exe_dir)
}
pub fn resolve_resource_path(app_handle: &tauri::AppHandle, relative_path: &str) -> Option<PathBuf> {
find_build_assets_dir(app_handle).map(|dir| dir.join(relative_path))
}
fn diagnose_environment(app_handle: &tauri::AppHandle) {
log::info!("--- [DIAGNOSIS] Environment Check Start ---");
// 1. Build Assets Dir
match find_build_assets_dir(app_handle) {
Some(path) => log::info!("✅ build_assets found at: {:?}", path),
None => log::error!("❌ build_assets NOT FOUND. Sidecar and resources will fail."),
}
// 2. Critical Files Check
let critical_files = ["mcp.json", "config.json", "vector.dll", "llama.dll"];
for file in critical_files {
match resolve_resource_path(app_handle, file) {
Some(p) if p.exists() => log::info!("✅ found {}: {:?}", file, p),
Some(p) => log::error!("❌ missing {}: path resolved to {:?} but file does not exist", file, p),
None => log::error!("❌ could not resolve path for {}", file),
}
}
// 3. Model file check (sample)
if let Some(base) = find_build_assets_dir(app_handle) {
let model_dir = base.join("models");
if model_dir.exists() {
log::info!("✅ models directory exists at: {:?}", model_dir);
} else {
log::warn!("⚠️ models directory NOT found at: {:?}", model_dir);
}
}
log::info!("--- [DIAGNOSIS] Environment Check End ---");
}
#[tauri::command]
fn get_mcp_info(app_handle: tauri::AppHandle) -> Result<serde_json::Value, String> {
let mcp_path = resolve_resource_path(&app_handle, "mcp.json")
.ok_or_else(|| "Failed to resolve mcp.json path".to_string())?;
if !mcp_path.exists() {
return Err(format!("mcp.json not found at {:?}", mcp_path));
}
let content = std::fs::read_to_string(&mcp_path).map_err(|e| e.to_string())?;
let mcp_data: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
Ok(mcp_data)
}
#[tauri::command]
async fn get_db_stats(state: tauri::State<'_, Arc<AppState>>) -> Result<serde_json::Value, String> {
use crate::entities::items;
use sea_orm::{EntityTrait, PaginatorTrait};
let count = items::Entity::find().count(&state.db).await.map_err(|e| e.to_string())?;
Ok(serde_json::json!({ "itemCount": count }))
}
#[tauri::command]
async fn get_sidecar_status(state: tauri::State<'_, Arc<AppState>>) -> Result<bool, String> {
Ok(state.llama.check_health().await)
}
// --- DB Browser Commands ---
fn validate_table_name(table_name: &str) -> Result<(), String> {
if table_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
Ok(())
} else {
Err("Invalid table name".to_string())
}
}
#[tauri::command]
async fn get_table_list(state: tauri::State<'_, Arc<AppState>>) -> Result<Vec<String>, String> {
let results = state.db.query_all(Statement::from_string(
DatabaseBackend::Sqlite,
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';".to_string(),
)).await.map_err(|e| e.to_string())?;
Ok(results.into_iter().map(|r| r.try_get::<String>("", "name").unwrap_or_default()).collect())
}
#[tauri::command]
async fn get_table_schema(state: tauri::State<'_, Arc<AppState>>, table: String) -> Result<serde_json::Value, String> {
validate_table_name(&table)?;
let query = format!("PRAGMA table_info({});", table);
let results = state.db.query_all(Statement::from_string(
DatabaseBackend::Sqlite,
query,
)).await.map_err(|e| e.to_string())?;
let mut schema = Vec::new();
for row in results {
schema.push(serde_json::json!({
"name": row.try_get::<String>("", "name").unwrap_or_default(),
"type": row.try_get::<String>("", "type").unwrap_or_default(),
"notnull": row.try_get::<i64>("", "notnull").unwrap_or(0),
"pk": row.try_get::<i64>("", "pk").unwrap_or(0),
}));
}
Ok(serde_json::json!(schema))
}
#[tauri::command]
async fn get_table_data(state: tauri::State<'_, Arc<AppState>>, table: String, limit: u64, offset: u64) -> Result<serde_json::Value, String> {
validate_table_name(&table)?;
let query = format!("SELECT * FROM {} LIMIT {} OFFSET {};", table, limit, offset);
let results = state.db.query_all(Statement::from_string(
DatabaseBackend::Sqlite,
query,
)).await.map_err(|e| e.to_string())?;
let mut data = Vec::new();
for row in results {
let mut item = serde_json::Map::new();
// Since we don't know column names dynamically easily with sea-orm query_all without knowing schema,
// we might just return keys from the first result if we had them.
// For now, let's keep it simple or use a specialized query.
item.insert("id".to_string(), serde_json::json!(row.try_get::<i64>("", "id").unwrap_or(0)));
if let Ok(content) = row.try_get::<String>("", "content") {
item.insert("content".to_string(), serde_json::json!(content));
}
data.push(serde_json::Value::Object(item));
}
Ok(serde_json::json!(data))
}
// --- Configuration ---
#[derive(serde::Deserialize, Clone, Debug)]
pub struct AppConfig {
pub database: DatabasePathConfig,
pub model: ModelConfig,
pub llama_server: LlamaServerConfig,
}
#[derive(serde::Deserialize, Clone, Debug)]
pub struct DatabasePathConfig {
pub path: String,
}
#[derive(serde::Deserialize, Clone, Debug)]
pub struct ModelConfig {
pub path: String,
}
#[derive(serde::Deserialize, Clone, Debug)]
pub struct LlamaServerConfig {
pub port: u16,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
database: DatabasePathConfig { path: "data/vector.db".to_string() },
model: ModelConfig { path: "models/embeddinggemma-300m-q4_0.gguf".to_string() },
llama_server: LlamaServerConfig { port: 8080 },
}
}
}
pub fn get_config(app_handle: &tauri::AppHandle) -> AppConfig {
let config_path = resolve_resource_path(app_handle, "config.json");
if let Some(path) = config_path {
if path.exists() {
if let Ok(content) = std::fs::read_to_string(path) {
if let Ok(config) = serde_json::from_str(&content) {
return config;
}
}
}
}
AppConfig::default()
}
pub fn resolve_db_path(app_handle: &tauri::AppHandle, config: &AppConfig) -> String {
let base_dir = app_handle.path().app_data_dir().unwrap_or_else(|_| PathBuf::from("."));
if !base_dir.exists() {
let _ = std::fs::create_dir_all(&base_dir);
}
let db_path = base_dir.join(&config.database.path);
if let Some(parent) = db_path.parent() {
if !parent.exists() {
let _ = std::fs::create_dir_all(parent);
}
}
db_path.to_string_lossy().to_string()
}
pub fn resolve_extension_path(app_handle: &tauri::AppHandle) -> String {
let ext_path = resolve_resource_path(app_handle, "vector.dll");
match ext_path {
Some(p) => p.to_string_lossy().to_string(),
None => "vector".to_string(),
}
}
// --- Sidecar ---
pub fn spawn_llama_server(app_handle: &tauri::AppHandle, config: &AppConfig) {
use tauri_plugin_shell::ShellExt;
let base_dir = find_build_assets_dir(app_handle).unwrap_or_else(|| {
log::error!("Failed to find build_assets for sidecar");
PathBuf::from(".")
});
let model_path = base_dir.join(&config.model.path);
log::info!("Starting llama-server with model: {:?}", model_path);
if !model_path.exists() {
log::error!("❌ Model file does NOT exist: {:?}", model_path);
return;
}
let port_str = config.llama_server.port.to_string();
// Add build_assets to PATH so llama-server can find its DLLs (ggml.dll, etc.)
let old_path = std::env::var("PATH").unwrap_or_default();
let new_path = format!("{};{}", base_dir.display(), old_path).replace("\\\\?\\", "");
match app_handle.shell().sidecar("llama-server") {
Ok(sidecar) => {
let sidecar = sidecar
.args(["--model", model_path.to_str().unwrap_or(""), "--port", &port_str, "--embedding", "--host", "127.0.0.1", "-c", "8192", "-b", "8192", "-ub", "8192", "--parallel", "1"])
.env("PATH", &new_path);
let (mut rx, _child) = sidecar.spawn().expect("Failed to spawn llama-server sidecar");
tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line) => {
log::debug!("llama-server stdout: {}", String::from_utf8_lossy(&line));
}
tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
log::debug!("llama-server stderr: {}", String::from_utf8_lossy(&line));
}
_ => {}
}
}
});
}
Err(e) => {
log::error!("Failed to initialize llama-server sidecar: {}", e);
}
}
}
// --- Run ---
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_log::Builder::new().target(tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir {
file_name: Some("TelosDB".to_string()),
})).build())
.setup(|app| {
log::info!("Application starting (Tauri 2)...");
// 2. DIAGNOSIS
diagnose_environment(app.handle());
dotenv().ok();
let app_handle = app.handle().clone();
// 2.5 Update process PATH to include build_assets directory so DLLs can find each other
if let Some(assets_dir) = find_build_assets_dir(&app_handle) {
if assets_dir.exists() {
let old_path = env::var("PATH").unwrap_or_default();
let new_path = format!("{};{}", assets_dir.display(), old_path).replace("\\\\?\\", "");
env::set_var("PATH", &new_path);
log::info!("Updated process PATH with build_assets: {}", new_path);
}
}
// 3. Start Sidecar
let config = get_config(&app_handle);
spawn_llama_server(&app_handle, &config);
// 4. Database and MCP initialization
let mcp_tx = tokio::sync::broadcast::channel(100).0;
let connection_count = Arc::new(AtomicUsize::new(0));
let llama = Arc::new(LlamaClient::new(
env::var("LLAMA_CPP_BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()),
env::var("LLAMA_CPP_EMBEDDING_MODEL").unwrap_or_else(|_| "nomic-embed-text".to_string()),
env::var("LLAMA_CPP_MODEL").unwrap_or_else(|_| "mistral".to_string()),
));
let app_handle_for_db = app.handle().clone();
let mcp_tx_for_db = mcp_tx.clone();
let connection_count_for_db = connection_count.clone();
let llama_for_db = llama.clone();
tauri::async_runtime::block_on(async move {
let db_path = resolve_db_path(&app_handle_for_db, &config);
let ext_path = resolve_extension_path(&app_handle_for_db);
log::info!("DB Path: {}, Ext Path: {}", db_path, ext_path);
let vec_dim = env::var("VEC_DIM").unwrap_or_else(|_| "768".to_string()).parse::<usize>().unwrap_or(768);
match db::init_db(&db_path, &ext_path, vec_dim).await {
Ok(conn) => {
log::info!("✅ Database initialized successfully");
let state = Arc::new(AppState {
db: conn,
llama: llama_for_db,
mcp_tx: mcp_tx_for_db,
connection_count: connection_count_for_db,
app_handle: app_handle_for_db.clone(),
});
app_handle_for_db.manage(state.clone());
let port = env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string()).parse::<u16>().unwrap_or(3000);
tokio::spawn(async move { mcp::start_mcp_server(state, port).await; });
}
Err(e) => {
log::error!("CRITICAL: Failed to initialize database: {}", e);
log::error!("This is likely due to SQLite extension load failure. Check vector.dll and its dependencies.");
panic!("Failed to init db: {}", e);
}
}
});
// 5. Tray setup
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let show_i = MenuItem::with_id(app, "show", "Show", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_i, &quit_i])?;
let mut tray_builder = TrayIconBuilder::new();
if let Some(icon) = app.default_window_icon() {
tray_builder = tray_builder.icon(icon.clone());
} else {
log::warn!("Default window icon not found, tray icon might be empty.");
}
let _tray = tray_builder
.menu(&menu)
.on_menu_event(|app, event| match event.id.as_ref() {
"quit" => app.exit(0),
"show" => if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.set_focus(); }
_ => {}
})
.on_tray_icon_event(|tray, event| if let TrayIconEvent::DoubleClick { .. } = event {
let app = tray.app_handle();
if let Some(w) = app.get_webview_window("main") { let _ = w.show(); let _ = w.set_focus(); }
})
.build(app)?;
log::info!("Tauri setup completed");
Ok(())
})
.invoke_handler(tauri::generate_handler![
get_mcp_info,
get_db_stats,
get_sidecar_status,
get_table_list,
get_table_schema,
get_table_data
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}