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)
|
||||
.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)
|
||||
}
|
||||
|
||||
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(
|
||||
pool: &Pool<Sqlite>,
|
||||
@@ -85,6 +163,29 @@ async fn save_message(
|
||||
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 сообщений
|
||||
async fn get_recent_history(
|
||||
pool: &Pool<Sqlite>,
|
||||
@@ -117,18 +218,32 @@ async fn get_recent_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
|
||||
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<String, Box<dyn std::error::Error>> {
|
||||
// Собираем полный вектор сообщений
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<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());
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
// Отправка в LLM
|
||||
match send_llm_request_with_history(
|
||||
&client,
|
||||
&api_url,
|
||||
&model,
|
||||
system_prompt.as_deref(), // Преобразуем Option<String> в Option<&str>
|
||||
system_prompt.as_deref(),
|
||||
summary.as_deref(),
|
||||
&history,
|
||||
)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user