aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOxbian <oxbian@mailbox.org>2025-03-02 18:54:59 -0500
committerOxbian <oxbian@mailbox.org>2025-03-02 18:54:59 -0500
commit25cf2d92f3198ba7541dad979eca1f9c1238ff04 (patch)
tree605e4bda26caeaf2e4e5a82c225f0028c22597a9
parent2c03f0c29f582e7c8b2bd99c1ffa0b1ca7c96eff (diff)
downloadNAI-25cf2d92f3198ba7541dad979eca1f9c1238ff04.tar.gz
NAI-25cf2d92f3198ba7541dad979eca1f9c1238ff04.zip
feat: llama.cpp -> ollama API + reading from stream
-rw-r--r--Cargo.toml1
-rw-r--r--config/chat-LLM.json5
-rw-r--r--config/resume-LLM.json5
-rw-r--r--config/system.json3
-rw-r--r--src/app/init.rs101
-rw-r--r--src/app/llm.rs98
-rw-r--r--src/app/mod.rs1
-rw-r--r--src/helper/init.rs14
-rw-r--r--src/main.rs1
-rw-r--r--src/ui/init.rs7
10 files changed, 171 insertions, 65 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 3d4c780..2069fb2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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);
ArKa projects. All rights to me, and your next child right arm.