diff --git a/.gitignore b/.gitignore index ea8c4bf..2b82d39 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ /target +/config.toml +/chat_history.db +/chat_history.db-shm +/chat_history.db-wal \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index eed4f03..323fcf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1398,6 +1398,7 @@ dependencies = [ "serde_json", "sqlx", "tokio", + "toml", ] [[package]] @@ -1579,6 +1580,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2120,6 +2130,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.3" @@ -2590,6 +2641,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/Cargo.toml b/Cargo.toml index f53ba80..f63cd20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ reqwest = { version = "0.12", features = ["json"] } # Сериализация / Десериализация JSON serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +toml = "0.8" # Парсинг аргументов CLI (флаги --session-id и т.д.) clap = { version = "4.5", features = ["derive"] } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..ace23ea --- /dev/null +++ b/src/config.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AppConfig { + pub api_url: String, + pub model: String, + pub max_history_messages: usize, // сколько последних сообщений подтягивать из БД + pub system_prompt: Option, // задел на системный промпт +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + api_url: "http://192.168.0.50:6969/v1/chat/completions".to_string(), + model: "qwen2.5-coder".to_string(), + max_history_messages: 10, + system_prompt: Some("You are a helpful programming assistant.".to_string()), + } + } +} + +impl AppConfig { + pub fn load_or_create>(path: P) -> Result> { + let path = path.as_ref(); + + if path.exists() { + // Если файл есть — читаем + let content = fs::read_to_string(path)?; + let config: AppConfig = toml::from_str(&content)?; + Ok(config) + } else { + // Если файла нет — создаём дефолтный + let default_config = AppConfig::default(); + let toml_string = toml::to_string_pretty(&default_config)?; + fs::write(path, toml_string)?; + println!("[Config]: Создан стандартный файл конфигурации {:?}", path); + Ok(default_config) + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 9f262ce..d3be9b8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +mod config; +use config::AppConfig; + use clap::Parser; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -10,20 +13,21 @@ use tokio::io::{AsyncBufReadExt, BufReader}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - #[arg(short, long, default_value = "default_session")] - session_id: String, + #[arg(short, long)] + session_id: Option, - #[arg( - short, - long, - default_value = "http://192.168.0.50:6969/v1/chat/completions" - )] - api_url: String, + #[arg(long)] + api_url: Option, - #[arg(short, long, default_value = "qwen2.5-coder")] - model: String, + #[arg(short, long)] + model: Option, + + #[arg(long)] + limit: Option, + + #[arg(short, long, default_value = "config.toml")] + config: String, - /// Путь к файлу базы данных SQLite #[arg(long, default_value = "sqlite:chat_history.db?mode=rwc")] db_url: String, } @@ -34,18 +38,17 @@ struct ChatMessage { content: String, } -/// Инициализация БД: создание таблицы, индексов и включение режима WAL +/// Инициализация БД (режим WAL) async fn init_db(db_url: &str) -> Result, Box> { let options = SqliteConnectOptions::from_str(db_url)? .create_if_missing(true) - .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal); // WAL режим для высокой параллельности + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal); let pool = SqlitePoolOptions::new() - .max_connections(10) // До 10 параллельных соединений к БД + .max_connections(10) .connect_with(options) .await?; - // Создаем таблицу и индекс, если их нет sqlx::query( r#" CREATE TABLE IF NOT EXISTS messages ( @@ -65,7 +68,7 @@ async fn init_db(db_url: &str) -> Result, Box, session_id: &str, @@ -82,11 +85,11 @@ async fn save_message( Ok(()) } -/// Выборка последних N (5) сообщений контекста в хронологическом порядке +/// Выборка последних N сообщений async fn get_recent_history( pool: &Pool, session_id: &str, - limit: i64, + limit: usize, ) -> Result, sqlx::Error> { let rows = sqlx::query_as::<_, (String, String)>( r#" @@ -98,11 +101,11 @@ async fn get_recent_history( ORDER BY id DESC LIMIT ? ) - ORDER BY id ASC + ORDER BY id ASC; "#, ) .bind(session_id) - .bind(limit * 2) + .bind(limit as i64) .fetch_all(pool) .await?; @@ -114,16 +117,34 @@ async fn get_recent_history( Ok(history) } -/// Отправка запроса с учетом накопленного контекста +/// Отправка запроса с контекстом в LLM async fn send_llm_request_with_history( client: &reqwest::Client, api_url: &str, model: &str, + system_prompt: Option<&str>, // <-- Добавили системный промпт history: &[ChatMessage], ) -> Result> { + // Собираем полный вектор сообщений + let mut full_messages = Vec::new(); + + // 1. Если system_prompt задан и не пустой, ставим его САМЫМ ПЕРВЫМ + if let Some(prompt) = system_prompt { + if !prompt.trim().is_empty() { + full_messages.push(ChatMessage { + role: "system".to_string(), + content: prompt.to_string(), + }); + } + } + + // 2. Добавляем историю из базы данных (user / assistant) + full_messages.extend_from_slice(history); + + // 3. Отправляем в payload уже полный вектор let payload = json!({ "model": model, - "messages": history, // Передаем весь сохраненный контекст + "messages": full_messages, "temperature": 0.7 }); @@ -145,25 +166,31 @@ async fn send_llm_request_with_history( } #[tokio::main] -async fn main() { +async fn main() -> Result<(), Box> { let args = Args::parse(); + + // 1. Загружаем или создаём config.toml + let file_config = AppConfig::load_or_create(&args.config)?; + + // 2. Объединяем аргументы CLI и значения из конфига + let api_url = args.api_url.unwrap_or(file_config.api_url); + let model = args.model.unwrap_or(file_config.model); + let history_limit = args.limit.unwrap_or(file_config.max_history_messages); + let session_id = args + .session_id + .unwrap_or_else(|| "default_session".to_string()); + + // 3. Клиент HTTP и подключение к БД let client = reqwest::Client::new(); - - // 1. Инициализируем БД - let db_pool = match init_db(&args.db_url).await { - Ok(pool) => pool, - Err(e) => { - eprintln!("Ошибка инициализации БД: {}", e); - return; - } - }; - + let db_pool = init_db(&args.db_url).await?; + let system_prompt = file_config.system_prompt; println!("=================================================="); - println!(" LLM CLI Agent with Memory (Stage 2)"); - println!(" Session ID: {}", args.session_id); - println!(" API URL: {}", args.api_url); - println!(" Model: {}", args.model); - println!(" Database: SQLite (WAL Mode)"); + println!(" LLM CLI Agent with Memory & Config"); + println!(" Session ID: {}", session_id); + println!(" API URL: {}", api_url); + println!(" Model: {}", model); + println!(" History: последние {} сообщений", history_limit); + println!(" Config: {}", args.config); println!("=================================================="); println!("Введите сообщение. Для выхода наберите 'exit' или 'quit'.\n"); @@ -171,7 +198,7 @@ async fn main() { let mut reader = BufReader::new(stdin); loop { - print!("[Session: {}] > ", args.session_id); + print!("[Session: {}] > ", session_id); io::stdout().flush().unwrap(); let mut input = String::new(); @@ -193,14 +220,14 @@ async fn main() { break; } - // 2. Сохраняем сообщение пользователя в БД - if let Err(e) = save_message(&db_pool, &args.session_id, "user", trimmed_input).await { + // Сохраняем запрос юзера + if let Err(e) = save_message(&db_pool, &session_id, "user", trimmed_input).await { eprintln!("[Ошибка записи в БД]: {}", e); continue; } - // 3. Достаем последние 5 сообщений из БД (включая только что сохраненное) - let history = match get_recent_history(&db_pool, &args.session_id, 5).await { + // Вычитываем контекст (N сообщений) + let history = match get_recent_history(&db_pool, &session_id, history_limit).await { Ok(h) => h, Err(e) => { eprintln!("[Ошибка чтения из БД]: {}", e); @@ -208,21 +235,24 @@ async fn main() { } }; - print!( - "[Запрос к LLM с контекстом ({} сообщ.)...]\r", - history.len() - ); + print!("[Запрос к LLM (контекст {} сообщ.)...]\r", history.len()); io::stdout().flush().unwrap(); - // 4. Отправляем контекст в LLM - match send_llm_request_with_history(&client, &args.api_url, &args.model, &history).await { + // Отправка в LLM + match send_llm_request_with_history( + &client, + &api_url, + &model, + system_prompt.as_deref(), // Преобразуем Option в Option<&str> + &history, + ) + .await + { Ok(reply) => { - // \r - в начало, \x1B[2K - очистить всю текущую строку терминала print!("\r\x1B[2K"); println!("[LLM]: {}\n", reply); - if let Err(e) = save_message(&db_pool, &args.session_id, "assistant", &reply).await - { + if let Err(e) = save_message(&db_pool, &session_id, "assistant", &reply).await { eprintln!("[Ошибка записи ответа в БД]: {}", e); } } @@ -232,4 +262,6 @@ async fn main() { } } } + + Ok(()) }