Added config file
This commit is contained in:
@@ -1 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
|
/config.toml
|
||||||
|
/chat_history.db
|
||||||
|
/chat_history.db-shm
|
||||||
|
/chat_history.db-wal
|
||||||
Generated
+60
@@ -1398,6 +1398,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1579,6 +1580,15 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -2120,6 +2130,47 @@ dependencies = [
|
|||||||
"tokio",
|
"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]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -2590,6 +2641,15 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.57.1"
|
version = "0.57.1"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ reqwest = { version = "0.12", features = ["json"] }
|
|||||||
# Сериализация / Десериализация JSON
|
# Сериализация / Десериализация JSON
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
toml = "0.8"
|
||||||
|
|
||||||
# Парсинг аргументов CLI (флаги --session-id и т.д.)
|
# Парсинг аргументов CLI (флаги --session-id и т.д.)
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
|||||||
@@ -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<String>, // задел на системный промпт
|
||||||
|
}
|
||||||
|
|
||||||
|
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<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
-52
@@ -1,3 +1,6 @@
|
|||||||
|
mod config;
|
||||||
|
use config::AppConfig;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -10,20 +13,21 @@ use tokio::io::{AsyncBufReadExt, BufReader};
|
|||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[arg(short, long, default_value = "default_session")]
|
#[arg(short, long)]
|
||||||
session_id: String,
|
session_id: Option<String>,
|
||||||
|
|
||||||
#[arg(
|
#[arg(long)]
|
||||||
short,
|
api_url: Option<String>,
|
||||||
long,
|
|
||||||
default_value = "http://192.168.0.50:6969/v1/chat/completions"
|
|
||||||
)]
|
|
||||||
api_url: String,
|
|
||||||
|
|
||||||
#[arg(short, long, default_value = "qwen2.5-coder")]
|
#[arg(short, long)]
|
||||||
model: String,
|
model: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
limit: Option<usize>,
|
||||||
|
|
||||||
|
#[arg(short, long, default_value = "config.toml")]
|
||||||
|
config: String,
|
||||||
|
|
||||||
/// Путь к файлу базы данных SQLite
|
|
||||||
#[arg(long, default_value = "sqlite:chat_history.db?mode=rwc")]
|
#[arg(long, default_value = "sqlite:chat_history.db?mode=rwc")]
|
||||||
db_url: String,
|
db_url: String,
|
||||||
}
|
}
|
||||||
@@ -34,18 +38,17 @@ struct ChatMessage {
|
|||||||
content: String,
|
content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Инициализация БД: создание таблицы, индексов и включение режима WAL
|
/// Инициализация БД (режим WAL)
|
||||||
async fn init_db(db_url: &str) -> Result<Pool<Sqlite>, Box<dyn std::error::Error>> {
|
async fn init_db(db_url: &str) -> Result<Pool<Sqlite>, Box<dyn std::error::Error>> {
|
||||||
let options = SqliteConnectOptions::from_str(db_url)?
|
let options = SqliteConnectOptions::from_str(db_url)?
|
||||||
.create_if_missing(true)
|
.create_if_missing(true)
|
||||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal); // WAL режим для высокой параллельности
|
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal);
|
||||||
|
|
||||||
let pool = SqlitePoolOptions::new()
|
let pool = SqlitePoolOptions::new()
|
||||||
.max_connections(10) // До 10 параллельных соединений к БД
|
.max_connections(10)
|
||||||
.connect_with(options)
|
.connect_with(options)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Создаем таблицу и индекс, если их нет
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
CREATE TABLE IF NOT EXISTS messages (
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
@@ -65,7 +68,7 @@ async fn init_db(db_url: &str) -> Result<Pool<Sqlite>, Box<dyn std::error::Error
|
|||||||
Ok(pool)
|
Ok(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Сохранение сообщения в историю
|
/// Сохранение реплики
|
||||||
async fn save_message(
|
async fn save_message(
|
||||||
pool: &Pool<Sqlite>,
|
pool: &Pool<Sqlite>,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
@@ -82,11 +85,11 @@ async fn save_message(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Выборка последних N (5) сообщений контекста в хронологическом порядке
|
/// Выборка последних N сообщений
|
||||||
async fn get_recent_history(
|
async fn get_recent_history(
|
||||||
pool: &Pool<Sqlite>,
|
pool: &Pool<Sqlite>,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
limit: i64,
|
limit: usize,
|
||||||
) -> Result<Vec<ChatMessage>, sqlx::Error> {
|
) -> Result<Vec<ChatMessage>, sqlx::Error> {
|
||||||
let rows = sqlx::query_as::<_, (String, String)>(
|
let rows = sqlx::query_as::<_, (String, String)>(
|
||||||
r#"
|
r#"
|
||||||
@@ -98,11 +101,11 @@ async fn get_recent_history(
|
|||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
)
|
)
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC;
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(session_id)
|
.bind(session_id)
|
||||||
.bind(limit * 2)
|
.bind(limit as i64)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -114,16 +117,34 @@ async fn get_recent_history(
|
|||||||
Ok(history)
|
Ok(history)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Отправка запроса с учетом накопленного контекста
|
/// Отправка запроса с контекстом в 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>, // <-- Добавили системный промпт
|
||||||
history: &[ChatMessage],
|
history: &[ChatMessage],
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
// Собираем полный вектор сообщений
|
||||||
|
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!({
|
let payload = json!({
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": history, // Передаем весь сохраненный контекст
|
"messages": full_messages,
|
||||||
"temperature": 0.7
|
"temperature": 0.7
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,25 +166,31 @@ async fn send_llm_request_with_history(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let args = Args::parse();
|
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();
|
let client = reqwest::Client::new();
|
||||||
|
let db_pool = init_db(&args.db_url).await?;
|
||||||
// 1. Инициализируем БД
|
let system_prompt = file_config.system_prompt;
|
||||||
let db_pool = match init_db(&args.db_url).await {
|
|
||||||
Ok(pool) => pool,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Ошибка инициализации БД: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("==================================================");
|
println!("==================================================");
|
||||||
println!(" LLM CLI Agent with Memory (Stage 2)");
|
println!(" LLM CLI Agent with Memory & Config");
|
||||||
println!(" Session ID: {}", args.session_id);
|
println!(" Session ID: {}", session_id);
|
||||||
println!(" API URL: {}", args.api_url);
|
println!(" API URL: {}", api_url);
|
||||||
println!(" Model: {}", args.model);
|
println!(" Model: {}", model);
|
||||||
println!(" Database: SQLite (WAL Mode)");
|
println!(" History: последние {} сообщений", history_limit);
|
||||||
|
println!(" Config: {}", args.config);
|
||||||
println!("==================================================");
|
println!("==================================================");
|
||||||
println!("Введите сообщение. Для выхода наберите 'exit' или 'quit'.\n");
|
println!("Введите сообщение. Для выхода наберите 'exit' или 'quit'.\n");
|
||||||
|
|
||||||
@@ -171,7 +198,7 @@ async fn main() {
|
|||||||
let mut reader = BufReader::new(stdin);
|
let mut reader = BufReader::new(stdin);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
print!("[Session: {}] > ", args.session_id);
|
print!("[Session: {}] > ", session_id);
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
|
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
@@ -193,14 +220,14 @@ async fn main() {
|
|||||||
break;
|
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);
|
eprintln!("[Ошибка записи в БД]: {}", e);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Достаем последние 5 сообщений из БД (включая только что сохраненное)
|
// Вычитываем контекст (N сообщений)
|
||||||
let history = match get_recent_history(&db_pool, &args.session_id, 5).await {
|
let history = match get_recent_history(&db_pool, &session_id, history_limit).await {
|
||||||
Ok(h) => h,
|
Ok(h) => h,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[Ошибка чтения из БД]: {}", e);
|
eprintln!("[Ошибка чтения из БД]: {}", e);
|
||||||
@@ -208,21 +235,24 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
print!(
|
print!("[Запрос к LLM (контекст {} сообщ.)...]\r", history.len());
|
||||||
"[Запрос к LLM с контекстом ({} сообщ.)...]\r",
|
|
||||||
history.len()
|
|
||||||
);
|
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
|
|
||||||
// 4. Отправляем контекст в LLM
|
// Отправка в LLM
|
||||||
match send_llm_request_with_history(&client, &args.api_url, &args.model, &history).await {
|
match send_llm_request_with_history(
|
||||||
|
&client,
|
||||||
|
&api_url,
|
||||||
|
&model,
|
||||||
|
system_prompt.as_deref(), // Преобразуем Option<String> в Option<&str>
|
||||||
|
&history,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(reply) => {
|
Ok(reply) => {
|
||||||
// \r - в начало, \x1B[2K - очистить всю текущую строку терминала
|
|
||||||
print!("\r\x1B[2K");
|
print!("\r\x1B[2K");
|
||||||
println!("[LLM]: {}\n", reply);
|
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);
|
eprintln!("[Ошибка записи ответа в БД]: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,4 +262,6 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user