diff --git a/chat_history.db b/chat_history.db deleted file mode 100644 index 4ebf78c..0000000 Binary files a/chat_history.db and /dev/null differ diff --git a/chat_history.db-shm b/chat_history.db-shm deleted file mode 100644 index 9cb5378..0000000 Binary files a/chat_history.db-shm and /dev/null differ diff --git a/chat_history.db-wal b/chat_history.db-wal deleted file mode 100644 index e98a4d2..0000000 Binary files a/chat_history.db-wal and /dev/null differ diff --git a/src/main.rs b/src/main.rs index d3be9b8..d3e9934 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,9 +65,87 @@ async fn init_db(db_url: &str) -> Result, Box, + messages_to_summarize: &[ChatMessage], +) -> Result> { + let mut prompt = String::new(); + + if let Some(old_sum) = current_summary { + prompt.push_str(&format!( + "УЖЕ ИЗВЕСТНАЯ ПАМЯТЬ О ПОЛЬЗОВАТЕЛЕ И ЗАДАЧАХ:\n{}\n\n", + old_sum + )); + } + + prompt.push_str("НОВЫЕ СООБЩЕНИЯ ДИАЛОГА:\n"); + for msg in messages_to_summarize { + if msg.role == "user" { + prompt.push_str(&format!("User: {}\n", msg.content)); + } + } + + prompt.push_str( + r#" +ИНСТРУКЦИЯ ПО ОБНОВЛЕНИЮ ПАМЯТИ: +Проанализируй диалог и обнови накопительный профиль памяти. + +ЧТО НУЖНО СОХРАНЯТЬ (ТОЛЬКО ИЗ СООБЩЕНИЙ ПОЛЬЗОВАТЕЛЯ / USER): +1. Личные данные пользователя (имя, возраст, профессия, хобби, предпочтения, ограничения). +2. Контекст задачи или темы (над чем работаем, текущие цели, формат ответа, который просит пользователь). +3. Любые точные вводные данные (числа, имена, даты, пароли/коды, списки, специфические термины). + +КРИТИЧЕСКИ ВАЖНЫЕ ПРАВИЛА: +- ИГНОРИРУЙ справочные ответы Ассистента (Assistant). Не сохраняй в память факты, стихи, статьи или ответы на энциклопедические вопросы, которые сгенерировал Ассистент. +- Сохраняй ТОЛЬКО то, что сообщил сам Пользователь, либо явные решения/итоги, к которым пришли. +- Пиши максимально кратко, сжато, в виде маркированного списка. +- Если новые сообщения не содержат новых фактов о пользователе или его задаче — оставь существующую память без изменений. +НЕ ЗАПИСЫВАЙ: + +О чем пользователь спрашивал или интересовался (например: НЕ пиши "Спрашивал про точки Лагранжа" или "Интересуется столицами"). Записывай только утвердительные факты о самом пользователе или данные. +"#, + ); + + let payload = json!({ + "model": model, + "messages": [ + { + "role": "system", + "content": "Ты — автономный модуль памяти ассистента. Твоя единственная цель — вытаскивать и структурировать личные данные, контекст задач и явные указания ПОЛЬЗОВАТЕЛЯ. Ты не ответишь на вопросы из чата, ты только ведешь блокнот фактов." + }, + {"role": "user", "content": prompt} + ], + "temperature": 0.0 + }); + + let response = client.post(api_url).json(&payload).send().await?; + let res_body: serde_json::Value = response.json().await?; + + if let Some(summary) = res_body["choices"][0]["message"]["content"].as_str() { + Ok(summary.to_string()) + } else { + Err("Не удалось сгенерировать резюме".into()) + } +} + /// Сохранение реплики async fn save_message( pool: &Pool, @@ -85,6 +163,29 @@ async fn save_message( Ok(()) } +/// Сохранение или обновление summary для сессии (UPSERT) +async fn save_session_summary( + pool: &Pool, + session_id: &str, + summary: &str, +) -> Result<(), sqlx::Error> { + sqlx::query( + r#" + INSERT INTO session_summaries (session_id, summary, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(session_id) DO UPDATE SET + summary = excluded.summary, + updated_at = CURRENT_TIMESTAMP; + "#, + ) + .bind(session_id) + .bind(summary) + .execute(pool) + .await?; + + Ok(()) +} + /// Выборка последних N сообщений async fn get_recent_history( pool: &Pool, @@ -117,18 +218,32 @@ async fn get_recent_history( Ok(history) } +async fn get_session_summary( + pool: &Pool, + session_id: &str, +) -> Result, sqlx::Error> { + let result = sqlx::query_as::<_, (String,)>( + "SELECT summary FROM session_summaries WHERE session_id = ?", + ) + .bind(session_id) + .fetch_optional(pool) + .await?; + + Ok(result.map(|(s,)| s)) +} + /// Отправка запроса с контекстом в LLM async fn send_llm_request_with_history( client: &reqwest::Client, api_url: &str, model: &str, - system_prompt: Option<&str>, // <-- Добавили системный промпт + system_prompt: Option<&str>, + summary: Option<&str>, history: &[ChatMessage], ) -> Result> { - // Собираем полный вектор сообщений let mut full_messages = Vec::new(); - // 1. Если system_prompt задан и не пустой, ставим его САМЫМ ПЕРВЫМ + // 1. Системный промпт if let Some(prompt) = system_prompt { if !prompt.trim().is_empty() { full_messages.push(ChatMessage { @@ -138,10 +253,19 @@ async fn send_llm_request_with_history( } } - // 2. Добавляем историю из базы данных (user / assistant) + // 2. Долгосрочная сжатая память (если есть) + if let Some(sum) = summary { + if !sum.trim().is_empty() { + full_messages.push(ChatMessage { + role: "system".to_string(), + content: format!("[Краткий контекст прошлых сообщений]: {}", sum), + }); + } + } + + // 3. Последние N реплик из истории full_messages.extend_from_slice(history); - // 3. Отправляем в payload уже полный вектор let payload = json!({ "model": model, "messages": full_messages, @@ -184,6 +308,7 @@ async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let db_pool = init_db(&args.db_url).await?; let system_prompt = file_config.system_prompt; + println!("=================================================="); println!(" LLM CLI Agent with Memory & Config"); println!(" Session ID: {}", session_id); @@ -192,7 +317,7 @@ async fn main() -> Result<(), Box> { println!(" History: последние {} сообщений", history_limit); println!(" Config: {}", args.config); println!("=================================================="); - println!("Введите сообщение. Для выхода наберите 'exit' или 'quit'.\n"); + println!("Введите сообщение. Служебные команды: /summary, /clear, exit\n"); let stdin = tokio::io::stdin(); let mut reader = BufReader::new(stdin); @@ -214,19 +339,42 @@ async fn main() -> Result<(), Box> { continue; } + // --- Служебные команды CLI --- if trimmed_input.eq_ignore_ascii_case("exit") || trimmed_input.eq_ignore_ascii_case("quit") { println!("Завершение работы."); break; } - // Сохраняем запрос юзера + if trimmed_input == "/summary" { + match get_session_summary(&db_pool, &session_id).await { + Ok(Some(s)) => println!("\n[Текущий Summary]:\n{}\n", s), + Ok(None) => println!("\n[Текущий Summary]: (пусто)\n"), + Err(e) => eprintln!("[Ошибка чтения summary]: {}\n", e), + } + continue; + } + + if trimmed_input == "/clear" { + let _ = sqlx::query("DELETE FROM messages WHERE session_id = ?") + .bind(&session_id) + .execute(&db_pool) + .await; + let _ = sqlx::query("DELETE FROM session_summaries WHERE session_id = ?") + .bind(&session_id) + .execute(&db_pool) + .await; + println!("\n[Память сессии '{}' очищена]\n", session_id); + continue; + } + + // 1. Сохраняем запрос юзера в историю if let Err(e) = save_message(&db_pool, &session_id, "user", trimmed_input).await { eprintln!("[Ошибка записи в БД]: {}", e); continue; } - // Вычитываем контекст (N сообщений) + // 2. Вычитываем историю и существующий summary let history = match get_recent_history(&db_pool, &session_id, history_limit).await { Ok(h) => h, Err(e) => { @@ -235,15 +383,41 @@ async fn main() -> Result<(), Box> { } }; + let mut summary = match get_session_summary(&db_pool, &session_id).await { + Ok(s) => s, + Err(e) => { + eprintln!("[Ошибка чтения summary из БД]: {}", e); + None + } + }; + + // 3. Авто-суммаризация: сжимаем контекст при накоплении >= 6 сообщений + if history.len() >= 6 { + print!("[Сжатие контекста в фоновую память...]\r"); + io::stdout().flush().unwrap(); + + if let Ok(new_summary) = + summarize_context(&client, &api_url, &model, summary.as_deref(), &history).await + { + if let Err(e) = save_session_summary(&db_pool, &session_id, &new_summary).await { + eprintln!("[Ошибка сохранения summary]: {}", e); + } else { + summary = Some(new_summary); + } + } + print!("\r\x1B[2K"); + } + + // 4. Отправляем запрос в LLM print!("[Запрос к LLM (контекст {} сообщ.)...]\r", history.len()); io::stdout().flush().unwrap(); - // Отправка в LLM match send_llm_request_with_history( &client, &api_url, &model, - system_prompt.as_deref(), // Преобразуем Option в Option<&str> + system_prompt.as_deref(), + summary.as_deref(), &history, ) .await