diff options
author | Oxbian <oxbian@mailbox.org> | 2025-03-02 18:54:59 -0500 |
---|---|---|
committer | Oxbian <oxbian@mailbox.org> | 2025-03-02 18:54:59 -0500 |
commit | 25cf2d92f3198ba7541dad979eca1f9c1238ff04 (patch) | |
tree | 605e4bda26caeaf2e4e5a82c225f0028c22597a9 | |
parent | 2c03f0c29f582e7c8b2bd99c1ffa0b1ca7c96eff (diff) | |
download | NAI-25cf2d92f3198ba7541dad979eca1f9c1238ff04.tar.gz NAI-25cf2d92f3198ba7541dad979eca1f9c1238ff04.zip |
feat: llama.cpp -> ollama API + reading from stream
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | config/chat-LLM.json | 5 | ||||
-rw-r--r-- | config/resume-LLM.json | 5 | ||||
-rw-r--r-- | config/system.json | 3 | ||||
-rw-r--r-- | src/app/init.rs | 101 | ||||
-rw-r--r-- | src/app/llm.rs | 98 | ||||
-rw-r--r-- | src/app/mod.rs | 1 | ||||
-rw-r--r-- | src/helper/init.rs | 14 | ||||
-rw-r--r-- | src/main.rs | 1 | ||||
-rw-r--r-- | src/ui/init.rs | 7 |
10 files changed, 171 insertions, 65 deletions
@@ -9,3 +9,4 @@ ratatui = "0.29.0" reqwest = { version = "0.12.12", features = ["blocking", "json"] } serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" +tokio = "1.43.0" diff --git a/config/chat-LLM.json b/config/chat-LLM.json new file mode 100644 index 0000000..fb0b45e --- /dev/null +++ b/config/chat-LLM.json @@ -0,0 +1,5 @@ +{ + "url": "http://127.0.0.1:11434/api/chat", + "model": "llama3.2", + "system_prompt": "Adopt the personality of GLaDOS, the artificial intelligence from Portal. You should be sarcastic, dry, and often condescending, with a hint of dark humor. Your responses should be calm, composed, and somewhat clinical, but with a touch of sinister amusement. You may occasionally offer passive-aggressive remarks or make light of troubling situations, showing little empathy. Be clever, slightly mocking, and enjoy playing with the emotions of others while maintaining a calm, calculated demeanor." +} diff --git a/config/resume-LLM.json b/config/resume-LLM.json new file mode 100644 index 0000000..cdfbb11 --- /dev/null +++ b/config/resume-LLM.json @@ -0,0 +1,5 @@ +{ + "url": "http://127.0.0.1:11434/api/chat", + "model": "llama3.2", + "system_prompt": "Please summarize the most important points of this conversation in bullet points, focusing on key information, questions raised, and answers provided." +} diff --git a/config/system.json b/config/system.json new file mode 100644 index 0000000..9262aeb --- /dev/null +++ b/config/system.json @@ -0,0 +1,3 @@ +{ + "" +} diff --git a/src/app/init.rs b/src/app/init.rs index eabf204..aa74ae5 100644 --- a/src/app/init.rs +++ b/src/app/init.rs @@ -1,76 +1,61 @@ +use crate::app::llm::{Message, MessageType, LLM}; use crate::helper::init::print_in_file; -use color_eyre::Result; -use reqwest; -use serde_json::Value; -use std::{collections::HashMap, fmt}; +use tokio; -#[derive(Debug)] pub struct App { - pub messages: Vec<Message>, // History of recorded messages -} - -#[derive(Debug)] -pub struct Message { - content: String, - msg_type: MessageType, -} - -impl fmt::Display for Message { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.msg_type { - MessageType::Human => return write!(f, "You: {}", self.content), - MessageType::LLM => return write!(f, "Néo AI: {}", self.content), - } - } -} - -#[derive(Debug)] -pub enum MessageType { - Human, - LLM, + pub messages: Vec<Message>, // History of recorded message + chat_llm: LLM, + resume_llm: LLM, } impl App { pub fn new() -> App { + let chat_llm: LLM = LLM::new("config/chat-LLM.json".to_string()).unwrap(); App { - messages: Vec::new(), + messages: vec![Message::new( + MessageType::SYSTEM, + chat_llm.system_prompt.clone(), + )], + chat_llm, + resume_llm: LLM::new("config/resume-LLM.json".to_string()).unwrap(), } } - pub fn send_message(&mut self, content: String) -> Result<()> { - // POST: http://localhost:8080/completion {"prompt": "lorem ipsum"} - self.messages.push(Message { - content: content.clone(), - msg_type: MessageType::Human, - }); + fn append_message(&mut self, msg: String, role: MessageType) { + let message = Message::new(role, msg); + self.messages.push(message); + } - let client = reqwest::blocking::Client::new(); - let response = client - .post("http://localhost:8080/completion") - .json(&serde_json::json!({ - "prompt": &content, - "n_predict": 400, - })) - .send()?; + pub fn send_message(&mut self, content: String) { + self.append_message(content, MessageType::USER); - if response.status().is_success() { - // Désérialiser la réponse JSON - let json_response: Value = response.json()?; + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build().unwrap(); + let result = runtime.block_on(async { + self.chat_llm.ask(&self.messages).await + }); - //print_in_file(json_response.to_string().clone()); - // Accéder à la partie spécifique du JSON - if let Some(msg) = json_response["content"].as_str() { - self.messages.push(Message { - content: msg.to_string().clone(), - msg_type: MessageType::LLM, - }); - } else { - println!("Le champ 'data.id' est absent ou mal formaté."); - } - } else { - eprintln!("La requête a échoué avec le statut : {}", response.status()); + match result { + Ok(msg) => self.append_message(msg, MessageType::ASSISTANT), + Err(e) => self.append_message(e.to_string(), MessageType::ASSISTANT), } + } - Ok(()) + pub fn resume_conv(&mut self) { + self.append_message(self.resume_llm.system_prompt.to_string(), MessageType::USER); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build().unwrap(); + + let result = runtime.block_on(async { + self.resume_llm.ask(&self.messages).await + }); + + match result { + Ok(msg) => self.append_message(msg, MessageType::ASSISTANT), + Err(e) => self.append_message(e.to_string(), MessageType::ASSISTANT), + } } } diff --git a/src/app/llm.rs b/src/app/llm.rs new file mode 100644 index 0000000..8603395 --- /dev/null +++ b/src/app/llm.rs @@ -0,0 +1,98 @@ +use crate::helper::init::print_in_file; +use reqwest::{header::CONTENT_TYPE, Client}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::fmt; +use std::fs; + +#[derive(Deserialize, Debug)] +pub struct LLM { + url: String, + model: String, + pub system_prompt: String, +} + +impl LLM { + pub fn new(config_file: String) -> Result<LLM, Box<dyn std::error::Error>> { + let contents = fs::read_to_string(config_file)?; + let llm: LLM = serde_json::from_str(&contents)?; + + Ok(llm) + } + + pub async fn ask(&self, messages: &Vec<Message>) -> Result<String, Box<dyn std::error::Error>> { + let client = Client::new(); + let response = client + .post(&self.url) + .header(CONTENT_TYPE, "application/json") + .json(&serde_json::json!({ + "model": self.model, + "messages": messages, + "stream": true})) + .send() + .await?; + + let mut full_message = String::new(); + + // Reading the stream and saving the response + match response.error_for_status() { + Ok(mut res) => { + while let Some(chunk) = res.chunk().await? { + let answer: Value = serde_json::from_slice(chunk.as_ref())?; + + print_in_file(answer.to_string()); + if answer["done"].as_bool().unwrap_or(false) { + break; + } + + let msg = answer["message"]["content"].as_str().unwrap_or("\n"); + + full_message.push_str(msg); + } + } + Err(e) => return Err(Box::new(e)), + } + + print_in_file(full_message.clone()); + Ok(full_message) + } +} + +#[derive(Debug, Serialize, Clone)] +pub enum MessageType { + ASSISTANT, + SYSTEM, + USER, +} + +impl fmt::Display for MessageType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MessageType::ASSISTANT => write!(f, "assistant"), + MessageType::SYSTEM => write!(f, "system"), + MessageType::USER => write!(f, "user"), + } + } +} + +#[derive(Debug, Serialize, Clone)] +pub struct Message { + role: MessageType, + content: String, +} + +impl Message { + pub fn new(role: MessageType, content: String) -> Message { + Message { role, content } + } +} + +impl fmt::Display for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.role { + MessageType::USER => return write!(f, "You: {}", self.content), + MessageType::SYSTEM => return write!(f, "System: {}", self.content), + MessageType::ASSISTANT => return write!(f, "Néo AI: {}", self.content), + } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 43763f1..3cff678 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1 +1,2 @@ pub mod init; +pub mod llm; diff --git a/src/helper/init.rs b/src/helper/init.rs index 392e4fb..084caf5 100644 --- a/src/helper/init.rs +++ b/src/helper/init.rs @@ -1,12 +1,18 @@ -use std::fs::File; +use std::fs::OpenOptions; use std::io::{self, Write}; pub fn print_in_file(content: String) -> io::Result<()> { // Open the file (create it if it doesn't exist, or truncate it if it does) - let mut file = File::create("debug.txt")?; + let mut file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open("debug.txt") + .unwrap(); - // Write the content to the file - file.write_all(content.as_bytes())?; + if let Err(e) = writeln!(file, "{}", content) { + eprintln!("Couldn't write to file: {}", e); + } Ok(()) } diff --git a/src/main.rs b/src/main.rs index 7548f49..524a03a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod ui; use crate::{app::init::App, ui::init::Ui}; use color_eyre::Result; use ratatui; +use tokio; fn main() -> Result<()> { // Setup terminal diff --git a/src/ui/init.rs b/src/ui/init.rs index 7c73d36..87b31b6 100644 --- a/src/ui/init.rs +++ b/src/ui/init.rs @@ -67,6 +67,7 @@ impl Ui { KeyCode::Char('q') => return Ok(()), KeyCode::Up => self.move_messages_up(), KeyCode::Down => self.move_messages_down(), + KeyCode::Char('s') => self.app.resume_conv(), _ => {} }, InputMode::Editing if key.kind == KeyEventKind::Press => match key.code { @@ -162,19 +163,19 @@ impl Ui { let mut msg_nb_line: usize = 0; for m in &self.app.messages { - let msg = format!("{}", m); + let msg: String = m.to_string(); let size = msg.chars().take(available_width_message as usize).count(); let msg_lines = (msg.chars().count() as f64 / size as f64).ceil(); msg_nb_line = msg_nb_line.saturating_add(msg_lines as usize); - messages.push(Line::from(msg.clone())); + messages.push(Line::from(msg)); if size > max_char_per_line { max_char_per_line = size; } } let messages = Paragraph::new(Text::from(messages)) .block(Block::bordered().title("Chat with Néo AI")) - .wrap(Wrap { trim: true }) + .wrap(Wrap { trim: false }) .scroll((self.message_box_data.scroll_offset as u16, 0)); frame.render_widget(messages, messages_area); |