Added summary of user dialogue
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
+184
-10
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user