Added summary of user dialogue

This commit is contained in:
2026-07-24 21:26:30 +03:00
parent 2cf553dfc9
commit d1e9a8e934
4 changed files with 184 additions and 10 deletions
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+184 -10
View File
@@ -65,9 +65,87 @@ async fn init_db(db_url: &str) -> Result<Pool<Sqlite>, Box<dyn std::error::Error
.execute(&pool) .execute(&pool)
.await?; .await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS session_summaries (
session_id TEXT PRIMARY KEY,
summary TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
"#,
)
.execute(&pool)
.await?;
Ok(pool) Ok(pool)
} }
async fn summarize_context(
client: &reqwest::Client,
api_url: &str,
model: &str,
current_summary: Option<&str>,
messages_to_summarize: &[ChatMessage],
) -> Result<String, Box<dyn std::error::Error>> {
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( async fn save_message(
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
@@ -85,6 +163,29 @@ async fn save_message(
Ok(()) Ok(())
} }
/// Сохранение или обновление summary для сессии (UPSERT)
async fn save_session_summary(
pool: &Pool<Sqlite>,
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 сообщений /// Выборка последних N сообщений
async fn get_recent_history( async fn get_recent_history(
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
@@ -117,18 +218,32 @@ async fn get_recent_history(
Ok(history) Ok(history)
} }
async fn get_session_summary(
pool: &Pool<Sqlite>,
session_id: &str,
) -> Result<Option<String>, 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 /// Отправка запроса с контекстом в LLM
async fn send_llm_request_with_history( async fn send_llm_request_with_history(
client: &reqwest::Client, client: &reqwest::Client,
api_url: &str, api_url: &str,
model: &str, model: &str,
system_prompt: Option<&str>, // <-- Добавили системный промпт system_prompt: Option<&str>,
summary: Option<&str>,
history: &[ChatMessage], history: &[ChatMessage],
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
// Собираем полный вектор сообщений
let mut full_messages = Vec::new(); let mut full_messages = Vec::new();
// 1. Если system_prompt задан и не пустой, ставим его САМЫМ ПЕРВЫМ // 1. Системный промпт
if let Some(prompt) = system_prompt { if let Some(prompt) = system_prompt {
if !prompt.trim().is_empty() { if !prompt.trim().is_empty() {
full_messages.push(ChatMessage { 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); full_messages.extend_from_slice(history);
// 3. Отправляем в payload уже полный вектор
let payload = json!({ let payload = json!({
"model": model, "model": model,
"messages": full_messages, "messages": full_messages,
@@ -184,6 +308,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let db_pool = init_db(&args.db_url).await?; let db_pool = init_db(&args.db_url).await?;
let system_prompt = file_config.system_prompt; let system_prompt = file_config.system_prompt;
println!("=================================================="); println!("==================================================");
println!(" LLM CLI Agent with Memory & Config"); println!(" LLM CLI Agent with Memory & Config");
println!(" Session ID: {}", session_id); println!(" Session ID: {}", session_id);
@@ -192,7 +317,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!(" History: последние {} сообщений", history_limit); println!(" History: последние {} сообщений", history_limit);
println!(" Config: {}", args.config); println!(" Config: {}", args.config);
println!("=================================================="); println!("==================================================");
println!("Введите сообщение. Для выхода наберите 'exit' или 'quit'.\n"); println!("Введите сообщение. Служебные команды: /summary, /clear, exit\n");
let stdin = tokio::io::stdin(); let stdin = tokio::io::stdin();
let mut reader = BufReader::new(stdin); let mut reader = BufReader::new(stdin);
@@ -214,19 +339,42 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
continue; continue;
} }
// --- Служебные команды CLI ---
if trimmed_input.eq_ignore_ascii_case("exit") || trimmed_input.eq_ignore_ascii_case("quit") if trimmed_input.eq_ignore_ascii_case("exit") || trimmed_input.eq_ignore_ascii_case("quit")
{ {
println!("Завершение работы."); println!("Завершение работы.");
break; 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 { if let Err(e) = save_message(&db_pool, &session_id, "user", trimmed_input).await {
eprintln!("[Ошибка записи в БД]: {}", e); eprintln!("[Ошибка записи в БД]: {}", e);
continue; continue;
} }
// Вычитываем контекст (N сообщений) // 2. Вычитываем историю и существующий summary
let history = match get_recent_history(&db_pool, &session_id, history_limit).await { let history = match get_recent_history(&db_pool, &session_id, history_limit).await {
Ok(h) => h, Ok(h) => h,
Err(e) => { Err(e) => {
@@ -235,15 +383,41 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
}; };
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()); print!("[Запрос к LLM (контекст {} сообщ.)...]\r", history.len());
io::stdout().flush().unwrap(); io::stdout().flush().unwrap();
// Отправка в LLM
match send_llm_request_with_history( match send_llm_request_with_history(
&client, &client,
&api_url, &api_url,
&model, &model,
system_prompt.as_deref(), // Преобразуем Option<String> в Option<&str> system_prompt.as_deref(),
summary.as_deref(),
&history, &history,
) )
.await .await