From a04b0e142fd8e06bffbc767cacf772372f53e8fd Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Sat, 11 Nov 2023 11:14:54 +0000 Subject: [PATCH 1/6] Parser conditional compilation --- crates/parser/Cargo.toml | 7 ++++- crates/parser/src/parser/mod.rs | 46 +++++++++++++++++++++++---------- vsce/package.json | 2 +- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/crates/parser/Cargo.toml b/crates/parser/Cargo.toml index 09c63d7..49c84aa 100644 --- a/crates/parser/Cargo.toml +++ b/crates/parser/Cargo.toml @@ -6,7 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -full = ["csv", "scraper", "syn", "rustpython-parser", "sqlparser"] +csv-parser = ["csv"] +html-parser = ["scraper"] +rust-parser = ["syn"] +python-parser = ["rustpython-parser"] +sql-parser = ["sqlparser"] +full = ["csv-parser", "html-parser", "python-parser", "rust-parser", "sql-parser"] [dependencies] anyhow = "1.0" diff --git a/crates/parser/src/parser/mod.rs b/crates/parser/src/parser/mod.rs index 5de128a..897a5ef 100644 --- a/crates/parser/src/parser/mod.rs +++ b/crates/parser/src/parser/mod.rs @@ -3,18 +3,18 @@ use std::fmt::Debug; use crate::err::ParseError; -pub mod json; -pub mod yaml; -#[cfg(feature = "full")] +#[cfg(feature = "csv-parser")] pub mod csv; -#[cfg(feature = "full")] +#[cfg(feature = "html-parser")] pub mod html; -#[cfg(feature = "full")] +pub mod json; +#[cfg(feature = "python-parser")] pub mod python; -#[cfg(feature = "full")] +#[cfg(feature = "rust-parser")] pub mod rust; -#[cfg(feature = "full")] +#[cfg(feature = "sql-parser")] pub mod sql; +pub mod yaml; /// A supertrait defining methods to convert LLM string outputs into various Rust /// native objects such as html, json, yaml, rust code, python code, etc. @@ -43,7 +43,11 @@ pub trait AsFormat { /// /// # Returns /// Result containing the desired Rust native object or a `ParseError`. - fn strip_format(&self, deserializer: F, format: &str) -> Result + fn strip_format( + &self, + deserializer: F, + format: &str, + ) -> Result where F: Fn(&str) -> Result + Copy, E: Into, @@ -59,7 +63,11 @@ pub trait AsFormat { /// /// # Returns /// Result containing a Vector of desired Rust native objects or a `ParseError`. - fn strip_formats(&self, deserializer: F, format: &str) -> Result, ParseError> + fn strip_formats( + &self, + deserializer: F, + format: &str, + ) -> Result, ParseError> where F: Fn(&str) -> Result + Copy, E: Into, @@ -99,7 +107,11 @@ impl<'a> AsFormat for &'a str { /// /// # Returns /// Result containing the desired Rust native object or a `ParseError`. - fn strip_format(&self, deserializer: F, format: &str) -> Result + fn strip_format( + &self, + deserializer: F, + format: &str, + ) -> Result where F: Fn(&str) -> Result + Copy, E: Into, @@ -156,7 +168,11 @@ impl<'a> AsFormat for &'a str { /// /// # Returns /// Result containing a Vector of desired Rust native objects or a `ParseError`. - fn strip_formats(&self, deserializer: F, format: &str) -> Result, ParseError> + fn strip_formats( + &self, + deserializer: F, + format: &str, + ) -> Result, ParseError> where F: Fn(&str) -> Result + Copy, E: Into, @@ -175,11 +191,15 @@ impl<'a> AsFormat for &'a str { while let Some(start_loc) = msg_.find(start_delimiter) { let start_index = start_loc + start_delimiter.len(); - let format = if let Some(end_loc) = msg_[start_index..].find(end_delimiter) { + let format = if let Some(end_loc) = + msg_[start_index..].find(end_delimiter) + { let end_index = start_index + end_loc; // Improve this code block, ideally no need to create extra string? - let format_string = msg_[start_index..end_index + end_delimiter.len()].to_string(); + let format_string = msg_ + [start_index..end_index + end_delimiter.len()] + .to_string(); msg_ = &msg_[end_index + end_delimiter.len()..]; format_string } else { diff --git a/vsce/package.json b/vsce/package.json index e5fcceb..72236fe 100644 --- a/vsce/package.json +++ b/vsce/package.json @@ -2,7 +2,7 @@ "name": "neatcoder", "displayName": "Neatwork AI - GPT4 Turbo on Steroids", "description": "Turn your IDE into an AI Sofware engineer.", - "version": "0.2.5", + "version": "9.9.9", "publisher": "NeatworkAi", "repository": { "url": "https://github.com/neatwork-ai/neatcoder-issues.git", From 9fa8598b0792559f2a3275658431b592f812bfa6 Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:13:48 +0000 Subject: [PATCH 2/6] Rearranged OpenAI client (Native Rust + WASM) --- Cargo.toml | 4 +- crates/neatcoder/Cargo.toml | 2 + .../neatcoder/src/endpoints/get_chat_title.rs | 33 +- .../src/endpoints/scaffold_project.rs | 23 +- crates/neatcoder/src/endpoints/stream_code.rs | 63 ++- crates/neatcoder/src/lib.rs | 364 ----------------- .../src/models/app_data/interfaces/apis.rs | 22 +- .../src/models/app_data/interfaces/dbs.rs | 22 +- .../src/models/app_data/interfaces/mod.rs | 8 +- .../src/models/app_data/interfaces/storage.rs | 22 +- crates/neatcoder/src/models/app_data/mod.rs | 8 +- .../src/models/app_data/task_pool/mod.rs | 6 +- .../models/app_data/task_pool/task_params.rs | 7 +- crates/neatcoder/src/models/chat.rs | 68 +-- .../src/openai/assistant/assistant.rs | 123 ------ crates/neatcoder/src/openai/assistant/mod.rs | 1 - .../neatcoder/src/openai/assistant/thread.rs | 0 crates/neatcoder/src/openai/mod.rs | 7 - crates/neatcoder/src/openai/msg.rs | 105 ----- crates/neatcoder/src/openai/request.rs | 230 ----------- crates/neatcoder/src/utils.rs | 33 +- crates/oai/Cargo.toml | 35 ++ crates/oai/src/lib.rs | 17 + .../src/models/chat/client.rs} | 0 crates/oai/src/models/chat/mod.rs | 3 + .../openai => oai/src/models/chat}/params.rs | 304 +++++++------- crates/oai/src/models/chat/request.rs | 386 ++++++++++++++++++ .../src/models/chat}/response.rs | 8 +- crates/oai/src/models/message.rs | 111 +++++ crates/oai/src/models/mod.rs | 43 ++ crates/oai/src/models/role.rs | 63 +++ .../src/openai => oai/src}/utils.rs | 0 crates/wasmer/Cargo.toml | 21 + crates/wasmer/src/lib.rs | 377 +++++++++++++++++ 34 files changed, 1335 insertions(+), 1184 deletions(-) delete mode 100644 crates/neatcoder/src/openai/assistant/assistant.rs delete mode 100644 crates/neatcoder/src/openai/assistant/mod.rs delete mode 100644 crates/neatcoder/src/openai/assistant/thread.rs delete mode 100644 crates/neatcoder/src/openai/mod.rs delete mode 100644 crates/neatcoder/src/openai/msg.rs delete mode 100644 crates/neatcoder/src/openai/request.rs create mode 100644 crates/oai/Cargo.toml create mode 100644 crates/oai/src/lib.rs rename crates/{neatcoder/src/openai/assistant/message.rs => oai/src/models/chat/client.rs} (100%) create mode 100644 crates/oai/src/models/chat/mod.rs rename crates/{neatcoder/src/openai => oai/src/models/chat}/params.rs (54%) create mode 100644 crates/oai/src/models/chat/request.rs rename crates/{neatcoder/src/openai => oai/src/models/chat}/response.rs (92%) create mode 100644 crates/oai/src/models/message.rs create mode 100644 crates/oai/src/models/mod.rs create mode 100644 crates/oai/src/models/role.rs rename crates/{neatcoder/src/openai => oai/src}/utils.rs (100%) create mode 100644 crates/wasmer/Cargo.toml create mode 100644 crates/wasmer/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 9f19b8b..162b52a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ [workspace] members = [ "crates/neatcoder", - "crates/parser" + "crates/parser", + "crates/oai", + "crates/wasmer" ] exclude = [ "examples/projects/*/jobs/codebase", diff --git a/crates/neatcoder/Cargo.toml b/crates/neatcoder/Cargo.toml index 88e4815..a9257f6 100644 --- a/crates/neatcoder/Cargo.toml +++ b/crates/neatcoder/Cargo.toml @@ -11,6 +11,8 @@ crate-type = ["cdylib"] [dependencies] parser = { path = "../parser" } +oai = { path = "../oai", features = ["wasm"] } +wasmer = { path = "../wasmer" } anyhow = "1.0" bytes = "1.4.0" diff --git a/crates/neatcoder/src/endpoints/get_chat_title.rs b/crates/neatcoder/src/endpoints/get_chat_title.rs index 99e8947..cbfd1d4 100644 --- a/crates/neatcoder/src/endpoints/get_chat_title.rs +++ b/crates/neatcoder/src/endpoints/get_chat_title.rs @@ -1,10 +1,12 @@ use anyhow::{anyhow, Result}; use js_sys::Function; - -use crate::openai::{ - msg::{GptRole, OpenAIMsg}, - params::{OpenAIModels, OpenAIParams}, - request::chat_raw, +use oai::models::{ + chat::{ + params::wasm::ChatParamsWasm as ChatParams, request::wasm::chat_raw, + }, + message::wasm::MessageWasm as AiMessage, + role::Role as GptRole, + Models as AiModels, }; pub async fn get_chat_title( @@ -13,16 +15,13 @@ pub async fn get_chat_title( ) -> Result { let mut prompts = Vec::new(); - prompts.push(OpenAIMsg { - role: GptRole::System, - content: String::from( - " + prompts.push(AiMessage::new(GptRole::System, String::from( + " - Context: Briefly describe the key topics or themes of the chat. - Title Specifications: The title should be concise, and not exceed 6 words. It should reflect the tone of the chat (e.g., professional, casual, informative, provocative, etc.). - Output: Provide a title that encapsulates the main focus of the chat. - ", - ), - }); + ", + ))); let main_prompt = format!( " @@ -33,15 +32,11 @@ The title of the prompt is:", msg ); - prompts.push(OpenAIMsg { - role: GptRole::User, - content: main_prompt, - }); + prompts.push(AiMessage::new(GptRole::User, main_prompt)); - let prompts = prompts.iter().map(|x| x).collect::>(); + let prompts = prompts.iter().map(|x| x).collect::>(); - let ai_params = - OpenAIParams::empty(OpenAIModels::Gpt35Turbo).max_tokens(15); + let ai_params = ChatParams::empty(AiModels::Gpt35Turbo).max_tokens(15); let chat = chat_raw(request_callback, &ai_params, &prompts, &[], &[]).await?; diff --git a/crates/neatcoder/src/endpoints/scaffold_project.rs b/crates/neatcoder/src/endpoints/scaffold_project.rs index f77fd21..bce0cfb 100644 --- a/crates/neatcoder/src/endpoints/scaffold_project.rs +++ b/crates/neatcoder/src/endpoints/scaffold_project.rs @@ -1,5 +1,9 @@ use anyhow::{anyhow, Result}; use js_sys::{Function, JsString}; +use oai::models::{ + chat::params::wasm::ChatParamsWasm as ChatParams, + message::wasm::MessageWasm as AiMessage, role::Role as GptRole, +}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::{ @@ -12,10 +16,6 @@ use wasm_bindgen::prelude::wasm_bindgen; use crate::{ consts::{CONFIG_EXTENSIONS, CONFIG_FILES}, models::app_data::language::Language, - openai::{ - msg::{GptRole, OpenAIMsg}, - params::OpenAIParams, - }, utils::write_json, }; @@ -41,18 +41,16 @@ impl ScaffoldParams { pub async fn scaffold_project( language: &Language, - ai_params: &OpenAIParams, + ai_params: &ChatParams, client_params: &ScaffoldParams, request_callback: &Function, ) -> Result<(Value, Files)> { let mut prompts = Vec::new(); - prompts.push(OpenAIMsg { - role: GptRole::System, - content: format!( + prompts.push(AiMessage::new(GptRole::System,format!( "You are a software engineer who is specialised in building software in {}.", language.name() ), - }); + )); // TODO: We should add the Database and API interfaces in previous messages, and add the name of the files here in order to index them let main_prompt = format!(" @@ -63,12 +61,9 @@ Based on the information provided write the project's folder structure, starting Answer in JSON format (Do not forget to start with ```json). For each file provide a brief description included in the json", language.name(), client_params.specs); - prompts.push(OpenAIMsg { - role: GptRole::User, - content: main_prompt, - }); + prompts.push(AiMessage::new(GptRole::User, main_prompt)); - let prompts = prompts.iter().map(|x| x).collect::>(); + let prompts = prompts.iter().map(|x| x).collect::>(); let (_, mut scaffold_json) = write_json(&ai_params, &prompts, request_callback).await?; diff --git a/crates/neatcoder/src/endpoints/stream_code.rs b/crates/neatcoder/src/endpoints/stream_code.rs index 77efded..2b3240c 100644 --- a/crates/neatcoder/src/endpoints/stream_code.rs +++ b/crates/neatcoder/src/endpoints/stream_code.rs @@ -1,18 +1,18 @@ use anyhow::{anyhow, Result}; use js_sys::JsString; +use oai::models::{ + chat::{ + params::wasm::ChatParamsWasm as ChatParams, request::request_stream, + }, + message::{wasm::MessageWasm as AiMessage, Message as AiMessageInner}, + role::Role as GptRole, +}; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, ops::Deref}; use wasm_bindgen::prelude::wasm_bindgen; +use wasmer::log; -use crate::{ - models::app_data::{interfaces::AsContext, AppData}, - openai::{ - msg::{GptRole, OpenAIMsg}, - params::OpenAIParams, - request::request_stream, - }, - utils::log, -}; +use crate::models::app_data::{interfaces::AsContext, AppData}; #[wasm_bindgen] #[derive(Debug, Deserialize, Serialize, Clone)] @@ -40,7 +40,7 @@ impl CodeGenParams { pub fn stream_code( app_state: &AppData, - ai_params: &OpenAIParams, + ai_params: &ChatParams, task_params: &CodeGenParams, codebase: BTreeMap, ) -> Result { @@ -72,35 +72,28 @@ pub fn stream_code( anyhow!("It seems that the the field `specs` is missing..") })?; - prompts.push(OpenAIMsg { - role: GptRole::System, - content: format!( + prompts.push(AiMessage::new( + GptRole::System, + format!( "You are a software engineer who is specialised in {}.", language.name() ), - }); + )); - prompts.push(OpenAIMsg { - role: GptRole::User, - content: String::from(project_description), - }); + prompts.push(AiMessage::new( + GptRole::User, + String::from(project_description), + )); for file in codebase.keys() { let code = codebase .get(file) .ok_or_else(|| anyhow!("Unable to find fild {:?}", file))?; - prompts.push(OpenAIMsg { - role: GptRole::User, - content: code.clone(), - }); + prompts.push(AiMessage::new(GptRole::User, code.clone())); } - prompts.push(OpenAIMsg { - role: GptRole::User, - // Needs to be optimized - content: project_scaffold.to_string(), - }); + prompts.push(AiMessage::new(GptRole::User, project_scaffold.to_string())); let mut main_prompt = format!( " @@ -132,14 +125,16 @@ pub fn stream_code( )); } - prompts.push(OpenAIMsg { - role: GptRole::User, - content: main_prompt, - }); + prompts.push(AiMessage::new(GptRole::User, main_prompt)); + + // Assuming prompts is a Vec<&AiMessageWasm> + let msgs: Vec<&AiMessageInner> = + prompts.iter().map(|m_wasm| (*m_wasm).deref()).collect(); - let prompts = prompts.iter().map(|x| x).collect::>(); + let prompts_slice: &[&AiMessageInner] = &msgs; - let request_body = request_stream(ai_params, &prompts, &[], &[])?; + let request_body = + request_stream(ai_params.deref(), &prompts_slice, &[], &[])?; Ok(request_body) } diff --git a/crates/neatcoder/src/lib.rs b/crates/neatcoder/src/lib.rs index d70c609..d6ff10f 100644 --- a/crates/neatcoder/src/lib.rs +++ b/crates/neatcoder/src/lib.rs @@ -1,370 +1,6 @@ -use chrono::{DateTime, Utc}; -use js_sys::{Date as IDate, Reflect}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use serde_wasm_bindgen::to_value; -use std::collections::{BTreeMap, HashMap, VecDeque}; -use std::hash::Hash; -use utils::log_err; -use wasm_bindgen::{JsCast, JsValue}; - pub mod consts; pub mod endpoints; pub mod models; -pub mod openai; pub mod prelude; pub mod typescript; pub mod utils; - -/// Type alias for JavaScript errors represented as JsValue -pub type JsError = JsValue; - -/// Trait to represent a type conversion between Rust and JavaScript types. -/// -/// This trait provides methods to convert between a Rust type and a corresponding -/// JavaScript type which can be cast from JsValue. -pub trait WasmType { - /// The type used in Rust representation - type RustType; - - /// Converts a Rust type into an external (JavaScript) type - /// - /// # Parameters - /// - `rust_type`: The Rust type to be converted. - /// - /// # Returns - /// - A Result containing the JavaScript type or an error. - fn to_extern(rust_type: Self::RustType) -> Result; - - /// Converts an external (JavaScript) type into a Rust type - /// - /// # Parameters - /// - `extern_type`: The JavaScript type to be converted. - /// - /// # Returns - /// - A Result containing the Rust type or an error. - fn from_extern(extern_type: ExternType) -> Result; -} - -/// Implementation of WasmType for BTreeMap with methods for converting between Rust and JavaScript types. -impl< - K: DeserializeOwned + Ord + Into + Clone, - V: Into + Clone, - ExternType: JsCast, - > WasmType for BTreeMap -where - V: DeserializeOwned, -{ - type RustType = BTreeMap; - - // Method to convert a BTreeMap to a JavaScript object representation - fn to_extern(rust_type: Self::RustType) -> Result { - // Create a new JavaScript object - let js_object = js_sys::Object::new(); - - // Set properties on the JavaScript object from the BTreeMap - for (key, value) in rust_type.iter() { - Reflect::set( - &js_object, - &key.clone().into(), - &value.clone().into(), - ) - .map_err(|e| { - JsValue::from_str(&format!( - "Failed to set property on JsValue: {:?}", - e - )) - })?; - } - - // Try to cast the JsValue to ExternType - // let ischemas: ExternType = js_object.dyn_into().map_err(|e| { - // JsValue::from_str(&format!( - // "Failed to cast Object `{:?}` to ExternType. Error: {:?}", - // js_object, e - // )) - // })?; - - // Unchecked here can potentially lead to problems down the line. - // However somehow `js_object.dyn_into()` messes up the casting.. - let extern_type = js_object.unchecked_into::(); - - Ok(extern_type) - } - - // Method to convert a JavaScript object representation to a BTreeMap - fn from_extern(extern_type: ExternType) -> Result { - let type_name = std::any::type_name::(); - - let js_value = extern_type.dyn_into::().map_err(|_| { - JsValue::from_str(&format!( - "Failed to convert {} to JsValue", - type_name - )) - })?; - - serde_wasm_bindgen::from_value(js_value).map_err(|e| { - let error_msg = format!( - "Failed to convert {} JsValue to BTreeMap: {:?}", - type_name, - std::any::type_name::(), - e, - ); - log_err(&error_msg); - JsValue::from_str(&error_msg) - }) - } -} - -/// Implementation of WasmType for VecDeque with methods for converting between Rust and JavaScript types. -impl< - V: DeserializeOwned + Serialize + Into + Clone, - ExternType: JsCast, - > WasmType for VecDeque -{ - type RustType = VecDeque; - - // Method to convert a VecDeque to a JavaScript object representation - fn to_extern(rust_type: Self::RustType) -> Result { - // Create a new JavaScript array - let js_array = js_sys::Array::new(); - - // Add elements from the VecDeque to the JavaScript array - // for value in rust_type.iter() { - // js_array.push(&value.clone().into()); - // } - - // Add elements from the VecDeque to the JavaScript object as array - for (index, value) in rust_type.iter().enumerate() { - let js_index = to_value(&(index as u32)).map_err(|e| { - JsError::from(JsValue::from_str(&format!( - "Failed to convert index to JsValue: {:?}", - e - ))) - })?; - - let js_value = to_value(&value.clone()).map_err(|e| { - JsError::from(JsValue::from_str(&format!( - "Failed to convert value to JsValue: {:?}", - e - ))) - })?; - - Reflect::set(&js_array, &js_index, &js_value).map_err(|e| { - JsValue::from_str(&format!( - "Failed to set property on JsValue: {:?}", - e - )) - })?; - } - - // Attempt to cast the JsValue (Array) to the ExternType - // We use unchecked_into here for the reasons mentioned in the BTreeMap implementation - let extern_type = js_array.unchecked_into::(); - - Ok(extern_type) - } - - // Method to convert a JavaScript object representation to a VecDeque - fn from_extern(extern_type: ExternType) -> Result { - let type_name = std::any::type_name::(); - - let js_value = extern_type.dyn_into::().map_err(|_| { - JsValue::from_str(&format!( - "Failed to convert {} to JsValue", - type_name - )) - })?; - - serde_wasm_bindgen::from_value(js_value).map_err(|e| { - let error_msg = format!( - "Failed to convert {} JsValue to VecDeque<{}>: {:?}", - type_name, - std::any::type_name::(), - e, - ); - log_err(&error_msg); - JsValue::from_str(&error_msg) - }) - } -} - -// TODO: dedup implementation of Vec/VecDeque -/// Implementation of WasmType for Vec with methods for converting between Rust and JavaScript types. -impl< - V: DeserializeOwned + Serialize + Into + Clone, - ExternType: JsCast, - > WasmType for Vec -{ - type RustType = Vec; - - // Add elements from the Vec to the JavaScript object as array - fn to_extern(rust_type: Self::RustType) -> Result { - // Create a new JavaScript array - let js_array = js_sys::Array::new(); - - // Add elements from the Vec to the JavaScript array - // for value in rust_type.iter() { - // js_array.push(&value.clone().into()); - // } - - // Add elements from the Vec to the JavaScript object as array - for (index, value) in rust_type.iter().enumerate() { - let js_index = to_value(&(index as u32)).map_err(|e| { - JsError::from(JsValue::from_str(&format!( - "Failed to convert index to JsValue: {:?}", - e - ))) - })?; - - let js_value = to_value(&value.clone()).map_err(|e| { - JsError::from(JsValue::from_str(&format!( - "Failed to convert value to JsValue: {:?}", - e - ))) - })?; - - Reflect::set(&js_array, &js_index, &js_value).map_err(|e| { - JsValue::from_str(&format!( - "Failed to set property on JsValue: {:?}", - e - )) - })?; - } - - // Attempt to cast the JsValue (Array) to the ExternType - // We use unchecked_into here for the reasons mentioned in the BTreeMap implementation - let extern_type = js_array.unchecked_into::(); - - Ok(extern_type) - } - - // Method to convert a JavaScript object representation to a VecDeque - fn from_extern(extern_type: ExternType) -> Result { - let type_name = std::any::type_name::(); - - let js_value = extern_type.dyn_into::().map_err(|_| { - JsValue::from_str(&format!( - "Failed to convert {} to JsValue", - type_name - )) - })?; - - serde_wasm_bindgen::from_value(js_value).map_err(|e| { - let error_msg = format!( - "Failed to convert {} JsValue to Vec<{}>: {:?}", - type_name, - std::any::type_name::(), - e, - ); - log_err(&error_msg); - JsValue::from_str(&error_msg) - }) - } -} - -// TODO: dedup implementation of BTreeMap/HashMap -/// Implementation of WasmType for BTreeMap with methods for converting between Rust and JavaScript types. -impl< - K: DeserializeOwned + Ord + Into + Clone + Hash, - V: Into + Clone, - ExternType: JsCast, - > WasmType for HashMap -where - V: DeserializeOwned, -{ - type RustType = HashMap; - - fn to_extern(rust_type: Self::RustType) -> Result { - // Create a new JavaScript object - let js_object = js_sys::Object::new(); - - // Set properties on the JavaScript object from the HashMap - for (key, value) in rust_type.iter() { - Reflect::set( - &js_object, - &key.clone().into(), - &value.clone().into(), - ) - .map_err(|e| { - JsValue::from_str(&format!( - "Failed to set property on JsValue: {:?}", - e - )) - })?; - } - - // Try to cast the JsValue to ExternType - // let ischemas: ExternType = js_object.dyn_into().map_err(|e| { - // JsValue::from_str(&format!( - // "Failed to cast Object `{:?}` to ExternType. Error: {:?}", - // js_object, e - // )) - // })?; - - // Unchecked here can potentially lead to problems down the line. - // However somehow `js_object.dyn_into()` messes up the casting.. - let extern_type = js_object.unchecked_into::(); - - Ok(extern_type) - } - - fn from_extern(extern_type: ExternType) -> Result { - let type_name = std::any::type_name::(); - - let js_value = extern_type.dyn_into::().map_err(|_| { - JsValue::from_str(&format!( - "Failed to convert {} to JsValue", - type_name - )) - })?; - - serde_wasm_bindgen::from_value(js_value).map_err(|e| { - let error_msg = format!( - "Failed to convert {} JsValue to HashMap: {:?}", - type_name, - std::any::type_name::(), - e, - ); - log_err(&error_msg); - JsValue::from_str(&error_msg) - }) - } -} - -impl WasmType for DateTime { - type RustType = DateTime; - - fn to_extern(rust_type: Self::RustType) -> Result { - // Convert the Rust DateTime to a string - let iso_string = rust_type.to_rfc3339(); - // Create a new JavaScript Date object from the string - let js_date = js_sys::Date::new(&JsValue::from_str(&iso_string)); - - // Check if the conversion was successful - if js_date.is_instance_of::() { - Ok(js_date) - } else { - Err(JsError::from( - "Failed to create JavaScript Date object.".to_owned(), - )) - } - } - - fn from_extern(extern_type: IDate) -> Result { - // Ensure we have a Date object - if let Some(date) = extern_type.dyn_into::().ok() { - // Convert the JavaScript Date object to an ISO string - let iso_string = - date.to_iso_string().as_string().unwrap_or_default(); - // Parse the ISO string into a Rust DateTime - DateTime::parse_from_rfc3339(&iso_string) - .map(|dt| dt.with_timezone(&Utc)) - .map_err(|e| JsError::from(e.to_string())) - } else { - Err(JsError::from( - "Input JsValue is not a Date object.".to_owned(), - )) - } - } -} diff --git a/crates/neatcoder/src/models/app_data/interfaces/apis.rs b/crates/neatcoder/src/models/app_data/interfaces/apis.rs index a6944c1..0320746 100644 --- a/crates/neatcoder/src/models/app_data/interfaces/apis.rs +++ b/crates/neatcoder/src/models/app_data/interfaces/apis.rs @@ -1,17 +1,17 @@ use super::{AsContext, SchemaFile}; -use crate::{ - openai::msg::{GptRole, OpenAIMsg}, - typescript::ISchemas, - JsError, WasmType, -}; +use crate::typescript::ISchemas; use anyhow::Result; use js_sys::JsString; +use oai::models::{ + message::wasm::MessageWasm as AiMessage, role::Role as GptRole, +}; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, fmt::{self, Display}, }; use wasm_bindgen::prelude::wasm_bindgen; +use wasmer::{JsError, WasmType}; /// Struct documenting an API interface. API here refers to interfaces of /// executables themselves or execution environments, and therefore it @@ -154,7 +154,7 @@ impl Api { } impl AsContext for Api { - fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { + fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { let mut main_prompt = format!( " Have in consideration the following {} communication service: @@ -172,20 +172,14 @@ Have in consideration the following {} communication service: main_prompt = format!("{}\n{} {}", main_prompt, "- host:", host); } - msg_sequence.push(OpenAIMsg { - role: GptRole::User, - content: main_prompt, - }); + msg_sequence.push(AiMessage::new(GptRole::User, main_prompt)); for (schema_name, schema) in self.schemas.iter() { let prompt = format!(" Consider the following schema as part of the {} database. It's called `{}` and the schema is:\n```\n{}``` ", self.name, schema_name, schema); - msg_sequence.push(OpenAIMsg { - role: GptRole::User, - content: prompt, - }); + msg_sequence.push(AiMessage::new(GptRole::User, prompt)); } Ok(()) diff --git a/crates/neatcoder/src/models/app_data/interfaces/dbs.rs b/crates/neatcoder/src/models/app_data/interfaces/dbs.rs index f0bb43a..28813d2 100644 --- a/crates/neatcoder/src/models/app_data/interfaces/dbs.rs +++ b/crates/neatcoder/src/models/app_data/interfaces/dbs.rs @@ -1,17 +1,17 @@ use super::{AsContext, SchemaFile}; -use crate::{ - openai::msg::{GptRole, OpenAIMsg}, - typescript::ISchemas, - JsError, WasmType, -}; +use crate::typescript::ISchemas; use anyhow::Result; use js_sys::JsString; +use oai::models::{ + message::wasm::MessageWasm as AiMessage, role::Role as GptRole, +}; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, fmt::{self, Display}, }; use wasm_bindgen::prelude::wasm_bindgen; +use wasmer::{JsError, WasmType}; /// Struct documenting a Database/DataWarehouse interface. This refers to Database /// storage solutions or to more classic Data Warehousing solutions such as @@ -232,7 +232,7 @@ pub enum DbType { } impl AsContext for Database { - fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { + fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { let mut main_prompt = format!( " Have in consideration the following {} Database: @@ -252,20 +252,14 @@ Have in consideration the following {} Database: format!("{}\n{} {}", main_prompt, "- database host:", host); } - msg_sequence.push(OpenAIMsg { - role: GptRole::User, - content: main_prompt, - }); + msg_sequence.push(AiMessage::new(GptRole::User, main_prompt)); for (schema_name, schema) in self.schemas.iter() { let prompt = format!(" Consider the following schema as part of the {} database. It's called `{}` and the schema is:\n```\n{}``` ", self.name, schema_name, schema); - msg_sequence.push(OpenAIMsg { - role: GptRole::User, - content: prompt, - }); + msg_sequence.push(AiMessage::new(GptRole::User, prompt)); } Ok(()) diff --git a/crates/neatcoder/src/models/app_data/interfaces/mod.rs b/crates/neatcoder/src/models/app_data/interfaces/mod.rs index c80d47e..fcfe29e 100644 --- a/crates/neatcoder/src/models/app_data/interfaces/mod.rs +++ b/crates/neatcoder/src/models/app_data/interfaces/mod.rs @@ -3,11 +3,13 @@ pub mod dbs; pub mod storage; use self::{apis::Api, dbs::Database, storage::Storage}; -use crate::{openai::msg::OpenAIMsg, typescript::ISchemas, JsError}; +use crate::typescript::ISchemas; use anyhow::{anyhow, Result}; +use oai::models::message::wasm::MessageWasm as AiMessage; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; +use wasmer::JsError; #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] @@ -169,11 +171,11 @@ pub type SchemaFile = String; /// Trait that injects the context of interfaces onto the LLM Message Sequence. pub trait AsContext { - fn add_context(&self, msg_sequence: &mut Vec) -> Result<()>; + fn add_context(&self, msg_sequence: &mut Vec) -> Result<()>; } impl AsContext for Interface { - fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { + fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { match self.interface_type { InterfaceType::Database => self .inner diff --git a/crates/neatcoder/src/models/app_data/interfaces/storage.rs b/crates/neatcoder/src/models/app_data/interfaces/storage.rs index 99a5f0a..e32b4fb 100644 --- a/crates/neatcoder/src/models/app_data/interfaces/storage.rs +++ b/crates/neatcoder/src/models/app_data/interfaces/storage.rs @@ -1,16 +1,16 @@ -use crate::{ - openai::msg::{GptRole, OpenAIMsg}, - typescript::ISchemas, - JsError, WasmType, -}; +use crate::typescript::ISchemas; use anyhow::Result; use js_sys::JsString; +use oai::models::{ + message::wasm::MessageWasm as AiMessage, role::Role as GptRole, +}; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, fmt::{self, Display}, }; use wasm_bindgen::prelude::wasm_bindgen; +use wasmer::{JsError, WasmType}; use super::{AsContext, SchemaFile}; @@ -141,7 +141,7 @@ impl Storage { } impl AsContext for Storage { - fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { + fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { let mut main_prompt = format!( " Have in consideration the following {} data storage: @@ -157,20 +157,14 @@ Have in consideration the following {} data storage: format!("{}\n{} {}", main_prompt, "- region:", region); } - msg_sequence.push(OpenAIMsg { - role: GptRole::User, - content: main_prompt, - }); + msg_sequence.push(AiMessage::new(GptRole::User, main_prompt)); for (schema_name, schema) in self.schemas.iter() { let prompt = format!(" Consider the following {} schema as part of the {} data storage. It's called `{}` and the schema is:\n```\n{}``` ", self.file_type, self.name, schema_name, schema); - msg_sequence.push(OpenAIMsg { - role: GptRole::User, - content: prompt, - }); + msg_sequence.push(AiMessage::new(GptRole::User, prompt)); } Ok(()) diff --git a/crates/neatcoder/src/models/app_data/mod.rs b/crates/neatcoder/src/models/app_data/mod.rs index 8076cd5..8c56ef8 100644 --- a/crates/neatcoder/src/models/app_data/mod.rs +++ b/crates/neatcoder/src/models/app_data/mod.rs @@ -7,16 +7,16 @@ use crate::{ scaffold_project::scaffold_project, stream_code::{stream_code, CodeGenParams}, }, - openai::params::OpenAIParams, typescript::{ICodebase, IInterfaces, ITasksVec}, - JsError, WasmType, }; use anyhow::{anyhow, Result}; use js_sys::JsString; use js_sys::{Error, Function}; +use oai::models::chat::params::wasm::ChatParamsWasm as ChatParams; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; +use wasmer::{JsError, WasmType}; use self::{ interfaces::{Interface, SchemaFile}, @@ -272,7 +272,7 @@ impl AppData { #[wasm_bindgen(js_name = scaffoldProject)] pub async fn scaffold_project( &mut self, - ai_params: &OpenAIParams, + ai_params: &ChatParams, task_params: TaskParams, request_callback: &Function, ) -> Result<(), JsError> { @@ -322,7 +322,7 @@ impl AppData { #[wasm_bindgen(js_name = streamCode)] pub fn stream_code( &mut self, - ai_params: &OpenAIParams, + ai_params: &ChatParams, task_params: TaskParams, codebase: ICodebase, ) -> Result { diff --git a/crates/neatcoder/src/models/app_data/task_pool/mod.rs b/crates/neatcoder/src/models/app_data/task_pool/mod.rs index 814209f..daddaa8 100644 --- a/crates/neatcoder/src/models/app_data/task_pool/mod.rs +++ b/crates/neatcoder/src/models/app_data/task_pool/mod.rs @@ -5,14 +5,12 @@ pub mod task; pub mod task_params; use self::{task::Task, task_params::TaskParams}; -use crate::{ - typescript::{IOrder, ITasks}, - JsError, WasmType, -}; +use crate::typescript::{IOrder, ITasks}; use anyhow::Result; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, VecDeque}; use wasm_bindgen::prelude::wasm_bindgen; +use wasmer::{JsError, WasmType}; /// Represents a pool of tasks. /// diff --git a/crates/neatcoder/src/models/app_data/task_pool/task_params.rs b/crates/neatcoder/src/models/app_data/task_pool/task_params.rs index 0f0e309..8e0bb12 100644 --- a/crates/neatcoder/src/models/app_data/task_pool/task_params.rs +++ b/crates/neatcoder/src/models/app_data/task_pool/task_params.rs @@ -3,15 +3,14 @@ //! It provides a `TaskParams` struct that represents parameters of a task, //! including the task type and associated inner parameters. -use crate::{ - endpoints::{scaffold_project::ScaffoldParams, stream_code::CodeGenParams}, - utils::log, - JsError, +use crate::endpoints::{ + scaffold_project::ScaffoldParams, stream_code::CodeGenParams, }; use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; use std::any::Any; use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; +use wasmer::{JsError, log}; /// Represents parameters for a task. /// diff --git a/crates/neatcoder/src/models/chat.rs b/crates/neatcoder/src/models/chat.rs index 3868161..1516847 100644 --- a/crates/neatcoder/src/models/chat.rs +++ b/crates/neatcoder/src/models/chat.rs @@ -1,63 +1,19 @@ use anyhow::Result; use chrono::{DateTime, Utc}; use js_sys::{Date as IDate, Function, JsString}; +use oai::models::{ + message::wasm::MessageWasm as AiMessage, Models as AiModels, +}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; use crate::{ endpoints::get_chat_title::get_chat_title, - openai::{msg::OpenAIMsg, params::OpenAIModels}, typescript::{IMessages, IModels}, - JsError, WasmType, }; -// TODO: Do we need to store all chates in a BTreeMap or just a -// reference to all chats? We could lazily read the chats as they're opened -// in the webview as opposed to having all the chats in the BTreeMap on start - -// #[wasm_bindgen] -// #[derive(Debug, Deserialize, Serialize, Clone)] -// #[serde(rename_all = "camelCase")] -// pub struct Chats(BTreeMap); - -// #[wasm_bindgen] -// impl Chats { -// #[wasm_bindgen(constructor)] -// pub fn new() -> Result { -// Ok(Self(BTreeMap::new())) -// } - -// #[wasm_bindgen(js_name = insertChat)] -// pub fn insert_chat(&mut self, chat: Chat) { -// self.insert(chat.session_id.clone(), chat); -// } - -// #[wasm_bindgen(js_name = removeChat)] -// pub fn remove_chat(&mut self, chat_id: String) { -// self.remove(&chat_id); -// } -// } - -// impl AsRef> for Chats { -// fn as_ref(&self) -> &BTreeMap { -// &self.0 -// } -// } - -// impl Deref for Chats { -// type Target = BTreeMap; - -// fn deref(&self) -> &Self::Target { -// &self.0 -// } -// } - -// impl DerefMut for Chats { -// fn deref_mut(&mut self) -> &mut Self::Target { -// &mut self.0 -// } -// } +use wasmer::{JsError, WasmType}; #[wasm_bindgen] #[derive(Debug, Deserialize, Serialize, Clone)] @@ -139,9 +95,13 @@ impl Chat { } #[wasm_bindgen(js_name = addModel)] - pub fn add_model(&mut self, model: OpenAIModels) { - let model_id = model.as_string(); - self.models.insert(model_id.clone(), Model::new(model_id)); + pub fn add_model(&mut self, model: AiModels) -> Result<(), JsError> { + let model_id = model.as_str(); + + self.models + .insert(model_id.to_string(), Model::new(model_id.to_string())); + + Ok(()) } #[wasm_bindgen(js_name = castFromString)] @@ -203,7 +163,7 @@ impl Model { pub struct Message { pub(crate) user: String, pub(crate) ts: DateTime, - pub(crate) payload: OpenAIMsg, + pub(crate) payload: AiMessage, } #[wasm_bindgen] @@ -212,7 +172,7 @@ impl Message { pub fn new( user: String, ts: IDate, - payload: OpenAIMsg, + payload: AiMessage, ) -> Result { let datetime = DateTime::from_extern(ts.into())?; Ok(Self { @@ -233,7 +193,7 @@ impl Message { } #[wasm_bindgen(getter)] - pub fn payload(&self) -> OpenAIMsg { + pub fn payload(&self) -> AiMessage { self.payload.clone() } } diff --git a/crates/neatcoder/src/openai/assistant/assistant.rs b/crates/neatcoder/src/openai/assistant/assistant.rs deleted file mode 100644 index 19d347a..0000000 --- a/crates/neatcoder/src/openai/assistant/assistant.rs +++ /dev/null @@ -1,123 +0,0 @@ -use anyhow::{anyhow, Result}; -use reqwest::{header::HeaderMap, Client}; -use serde::{ - de::{self, Visitor}, - Deserialize, Deserializer, Serialize, -}; -use serde_json::json; -use std::{collections::HashMap, fmt}; - -use crate::{consts::BASE_BETA_URL, openai::params::OpenAIModels}; - -#[derive(Serialize, Debug)] -pub struct AssistantRequest { - pub name: String, - pub instructions: String, - pub tools: Vec, // TODO: "tools": [{"type": "code_interpreter"}] - pub model: OpenAIModels, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Assistant { - id: String, - object: String, - created_at: u32, // TODO: Should be a timestamp - name: String, - description: Option, - model: OpenAIModels, - instructions: Option, - tools: Vec, - file_ids: Vec, - metadata: HashMap, -} - -#[derive(Debug, Serialize)] -pub enum Tool { - CodeInterpreter, - Retrieval, - FunctionCall, -} - -impl Tool { - pub fn new(tool: String) -> Self { - let tool = match tool.as_str() { - "code_interpreter" => Tool::CodeInterpreter, - "retrieval" => Tool::Retrieval, - "function" => Tool::FunctionCall, - _ => panic!("Invalid tool {}", tool), - }; - - tool - } - - pub fn as_string(&self) -> String { - match self { - Tool::CodeInterpreter => String::from("code_interpreter"), - Tool::Retrieval => String::from("retrieval"), - Tool::FunctionCall => String::from("function"), - } - } -} - -impl<'de> Deserialize<'de> for Tool { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct ToolVisitor; - - impl<'de> Visitor<'de> for ToolVisitor { - type Value = Tool; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string representing an OpenAI model") - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - match value { - "code_interpreter" => Ok(Tool::CodeInterpreter), - "retrieval" => Ok(Tool::Retrieval), - "function" => Ok(Tool::FunctionCall), - _ => Err(E::custom(format!( - "unexpected OpenAI tool: {}", - value - ))), - } - } - } - - deserializer.deserialize_str(ToolVisitor) - } -} - -impl AssistantRequest { - pub async fn create_assistant( - self, - client: &Client, - headers: &HeaderMap, - ) -> Result { - let response = client - .post(&format!("{}/assistants", BASE_BETA_URL)) - .headers(headers.clone()) - .json(&json!({ - "name": self.name, // "Math Tutor", - "instructions": self.instructions, // "You are a personal math tutor. Write and run code to answer math questions.", - "tools": self.tools, // [{"type": "code_interpreter"}], - "model": self.model, // "gpt-4-1106-preview" - })) - .send() - .await?; - - if response.status().is_success() { - let assistant = response.json::().await?; - println!("Create Assistant response: {:?}", assistant); - Ok(assistant) - } else { - // If not successful, perhaps you want to parse it differently or handle the error - Err(anyhow!(response.status())) - } - } -} diff --git a/crates/neatcoder/src/openai/assistant/mod.rs b/crates/neatcoder/src/openai/assistant/mod.rs deleted file mode 100644 index 09db8a7..0000000 --- a/crates/neatcoder/src/openai/assistant/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod assistant; diff --git a/crates/neatcoder/src/openai/assistant/thread.rs b/crates/neatcoder/src/openai/assistant/thread.rs deleted file mode 100644 index e69de29..0000000 diff --git a/crates/neatcoder/src/openai/mod.rs b/crates/neatcoder/src/openai/mod.rs deleted file mode 100644 index e173aa0..0000000 --- a/crates/neatcoder/src/openai/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod assistant; -///< Client for interacting with the OpenAI API. -pub mod msg; -pub mod params; -pub mod request; -pub mod response; -pub mod utils; diff --git a/crates/neatcoder/src/openai/msg.rs b/crates/neatcoder/src/openai/msg.rs deleted file mode 100644 index a7321dd..0000000 --- a/crates/neatcoder/src/openai/msg.rs +++ /dev/null @@ -1,105 +0,0 @@ -use anyhow::{anyhow, Result}; -use js_sys::JsString; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use wasm_bindgen::prelude::wasm_bindgen; - -#[wasm_bindgen] -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct OpenAIMsg { - pub(crate) role: GptRole, - pub(crate) content: String, -} - -#[wasm_bindgen] -#[derive(Debug, Clone, Copy)] -pub enum GptRole { - System, - User, - Assistant, -} - -impl OpenAIMsg { - pub fn user(content: &str) -> Self { - Self { - role: GptRole::User, - content: String::from(content), - } - } - - pub fn system(content: &str) -> Self { - Self { - role: GptRole::System, - content: String::from(content), - } - } - - pub fn assistant(content: &str) -> Self { - Self { - role: GptRole::Assistant, - content: String::from(content), - } - } -} - -#[wasm_bindgen] -impl OpenAIMsg { - #[wasm_bindgen(getter)] - pub fn role(&self) -> JsString { - self.role.as_str().to_string().into() - } - - #[wasm_bindgen(getter)] - pub fn content(&self) -> JsString { - self.content.clone().into() - } -} - -impl Serialize for GptRole { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - GptRole::System => serializer.serialize_str("system"), - GptRole::User => serializer.serialize_str("user"), - GptRole::Assistant => serializer.serialize_str("assistant"), - } - } -} - -impl<'de> Deserialize<'de> for GptRole { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - - match s.as_str() { - "system" => Ok(GptRole::System), - "user" => Ok(GptRole::User), - "assistant" => Ok(GptRole::Assistant), - _ => panic!("Invalid variant `{:?}`", s.as_str()), - } - } -} - -impl GptRole { - pub fn new(role: &str) -> Result { - let role = match role { - "system" => GptRole::System, - "user" => GptRole::User, - "assistant" => GptRole::Assistant, - _ => return Err(anyhow!(format!("Invalid role {}", role))), - }; - - Ok(role) - } - - pub fn as_str(&self) -> &str { - match self { - GptRole::System => "system", - GptRole::User => "user", - GptRole::Assistant => "assistant", - } - } -} diff --git a/crates/neatcoder/src/openai/request.rs b/crates/neatcoder/src/openai/request.rs deleted file mode 100644 index 4f9675e..0000000 --- a/crates/neatcoder/src/openai/request.rs +++ /dev/null @@ -1,230 +0,0 @@ -use super::{msg::OpenAIMsg, params::OpenAIParams, response::ResponseBody}; -use crate::{typescript::IOpenAIMsg, utils::log, JsError, WasmType}; -use anyhow::{anyhow, Result}; -use js_sys::{Function, Promise}; -use serde_json::{json, Value}; -use serde_wasm_bindgen::from_value; -use std::ops::Deref; -use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; -use wasm_bindgen_futures::JsFuture; - -#[wasm_bindgen(js_name = requestBody)] -pub fn request_body_( - msgs: IOpenAIMsg, - job: OpenAIParams, - stream: bool, -) -> Result { - let msgs: Vec = Vec::from_extern(msgs).map_err(|e| { - JsValue::from_str(&format!( - "Failed to convert msgs to native Wasm type: {:?}", - e - )) - })?; - - // let job = OpenAIParams::default(); - - let mut data = json!({ - "model": job.model.as_string(), - "messages": msgs, - // "stop": self.stop, - }); - - if stream { - data["stream"] = serde_json::Value::Bool(true); - } - - let serialized_str = serde_json::to_string(&data).map_err(|e| { - JsValue::from_str(&format!("Failed to serialize to string: {:?}", e)) - })?; - - Ok(JsValue::from_str(&serialized_str)) -} - -pub async fn chat( - request_callback: &Function, - ai_params: impl Deref, - msgs: &[&OpenAIMsg], - funcs: &[&String], - stop_seq: &[String], -) -> Result { - log("[DEBUG] Getting Chat Raw..."); - let chat = - chat_raw(request_callback, ai_params, msgs, funcs, stop_seq).await?; - - log("[DEBUG] Got answer."); - - let answer = chat - .choices - .first() - .ok_or_else(|| anyhow!("LLM Respose seems to be empty :("))? - .message - .content - .as_str(); - - Ok(String::from(answer)) -} - -pub async fn chat_raw( - request_callback: &Function, - ai_params: impl Deref, - msgs: &[&OpenAIMsg], - funcs: &[&String], - stop_seq: &[String], -) -> Result { - let req_body = request_body(ai_params, msgs, funcs, stop_seq, false)?; - - let body_json = serde_json::to_string(&req_body)?; - - log("[DEBUG] Getting promise..."); - let js_promise: Promise = request_callback - .call1(&JsValue::NULL, &JsValue::from_str(&body_json)) - .map_err(|e| anyhow!("Error performing request callback: {:?}", e))? - .dyn_into() - .map_err(|e| { - anyhow!("Error processing request callback promise: {:?}", e) - })?; - log("[INFO] Prompting the LLM..."); - let res_js_value: JsValue = - JsFuture::from(js_promise).await.map_err(|e| { - anyhow!("Error processing request callback result: {:?}", e) - })?; - - log("[INFO] Receive response from LLM.."); - log(&format!("LLM response body: {:?}", res_js_value)); - - let body: Result = from_value(res_js_value); - - match body { - Ok(body) => Ok(body), - Err(e) => { - Err(anyhow!("Could not convert JsValue to Response: {:?}", e)) - } - } -} - -pub fn request_stream( - ai_params: impl Deref, - msgs: &[&OpenAIMsg], - funcs: &[&String], - stop_seq: &[String], -) -> Result { - let req_body = request_body(ai_params, msgs, funcs, stop_seq, true)?; - - let body_json = serde_json::to_string(&req_body)?; - - Ok(body_json) -} - -pub fn request_body( - job: impl Deref, - msgs: &[&OpenAIMsg], - // TODO: Add to OpenAIParams - funcs: &[&String], - // TODO: Add to OpenAIParams - stop_seq: &[String], - stream: bool, -) -> Result { - let mut data = json!({ - "model": job.model.as_string(), - "messages": msgs, - // "stop": self.stop, - }); - - if let Some(temperature) = job.temperature { - data["temperature"] = serde_json::to_value(temperature)?; - } - - if let Some(top_p) = job.top_p { - data["top_p"] = serde_json::to_value(top_p)?; - } - - if !funcs.is_empty() { - data["functions"] = serde_json::to_value(funcs)?; - } - - if !stop_seq.is_empty() { - if stop_seq.len() > 4 { - return Err(anyhow!( - "Maximum limit of stop sequences reached. {} out of 4", - stop_seq.len() - )); - }; - - data["stop"] = serde_json::to_value(stop_seq)?; - } - - if let Some(user) = &job.user { - data["user"] = serde_json::to_value(user)?; - } - - if !job.logit_bias.is_empty() { - data["logit_bias"] = serde_json::to_value(&job.logit_bias)?; - } - - if let Some(frequency_penalty) = &job.frequency_penalty { - data["frequency_penalty"] = serde_json::to_value(frequency_penalty)?; - } - - if let Some(presence_penalty) = &job.presence_penalty { - data["presence_penalty"] = serde_json::to_value(presence_penalty)?; - } - - if let Some(max_tokens) = &job.max_tokens { - data["max_tokens"] = serde_json::to_value(max_tokens)?; - } - - if let Some(n) = &job.n { - data["n"] = serde_json::to_value(n)?; - } - - if stream { - data["stream"] = serde_json::Value::Bool(true); - } - - Ok(data) -} - -#[cfg(test)] -mod tests { - use crate::openai::response::ResponseBody; - use anyhow::Result; - use serde_wasm_bindgen::{from_value, to_value}; - use wasm_bindgen_test::wasm_bindgen_test; - - #[wasm_bindgen_test] - fn parse_js_callback_response() -> Result<()> { - let raw_response = r#" - { - "id":"chatcmpl-7wRVa4TZBNplfgGR6vfm6mT3j7uFT", - "object":"chat.completion", - "created":1694163122, - "model":"gpt-3.5-turbo-16k-0613", - "choices":[ - { - "index":0, - "message":{ - "role":"assistant", - "content":"```json\n{\n \"src\": {\n \"main.rs\": \"The main entry point of the Rust application\",\n \"config.rs\": \"A module for loading and managing configuration\",\n \"database.rs\": \"A module for handling database connection and queries\",\n \"models.rs\": \"A module defining the data models used in the application\",\n \"handlers\": {\n \"mod.rs\": \"A module for defining request handlers\",\n \"product_handler.rs\": \"A module for handling product-related requests\",\n \"order_handler.rs\": \"A module for handling order-related requests\",\n \"cart_handler.rs\": \"A module for handling shopping cart-related requests\"\n },\n \"middlewares\": {\n \"mod.rs\": \"A module for defining middlewares\",\n \"authentication.rs\": \"A middleware for handling authentication\",\n \"authorization.rs\": \"A middleware for handling authorization\"\n },\n \"routes\": {\n \"mod.rs\": \"A module for defining API routes\",\n \"product_routes.rs\": \"A module for defining product-related routes\",\n \"order_routes.rs\": \"A module for defining order-related routes\",\n \"cart_routes.rs\": \"A module for defining shopping cart-related routes\"\n },\n \"errors.rs\": \"A module defining custom error types and error handling\",\n \"util.rs\": \"A module containing utility functions used throughout the application\"\n }\n}\n```" - }, - "finish_reason":"stop" - } - ], - "usage":{ - "prompt_tokens":112, - "completion_tokens":282, - "total_tokens":394 - } - }"#; - - let json_value: ResponseBody = serde_json::from_str(raw_response)?; - let js_value = to_value(&json_value).unwrap(); - - let body: Result = from_value(js_value); - - if let Err(e) = body { - panic!("Upsie: {:?}", e); - } - - Ok(()) - } -} diff --git a/crates/neatcoder/src/utils.rs b/crates/neatcoder/src/utils.rs index 835761a..1f7f4cb 100644 --- a/crates/neatcoder/src/utils.rs +++ b/crates/neatcoder/src/utils.rs @@ -1,20 +1,17 @@ ///< Contains utility functions and helpers. -use crate::openai::request::chat_raw; -use crate::openai::{msg::OpenAIMsg, params::OpenAIParams}; -use crate::JsError; use anyhow::{anyhow, Result}; use js_sys::Function; +use oai::models::chat::{ + params::wasm::ChatParamsWasm as ChatParams, request::wasm::chat_raw, +}; +use oai::models::message::wasm::MessageWasm as GptMessage; use parser::parser::json::AsJson; -use serde::de::DeserializeOwned; use serde_json::Value; -use std::collections::HashMap; -use std::hash::Hash; -use wasm_bindgen::JsValue; -use web_sys::console; +use wasmer::log; pub async fn write_json( - ai_params: &OpenAIParams, - prompts: &Vec<&OpenAIMsg>, + ai_params: &ChatParams, + prompts: &Vec<&GptMessage>, request_callback: &Function, ) -> Result<(String, Value)> { let mut retries = 3; @@ -51,19 +48,3 @@ pub async fn write_json( } } } - -// TODO: This function is on life support and it will be removed in the next serde generalisatoin cycles. -pub fn jsvalue_to_hmap( - value: JsValue, -) -> Result, JsError> { - serde_wasm_bindgen::from_value(value) - .map_err(|e| JsError::from_str(&e.to_string())) -} - -pub fn log(msg: &str) { - console::log_1(&JsValue::from_str(msg)); -} - -pub fn log_err(msg: &str) { - console::error_1(&JsValue::from_str(&msg)); -} diff --git a/crates/oai/Cargo.toml b/crates/oai/Cargo.toml new file mode 100644 index 0000000..24f7f43 --- /dev/null +++ b/crates/oai/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "oai" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +path = "src/lib.rs" +crate-type = ["lib", "cdylib"] + +[features] +default = ["reqwest", "futures", "bytes", "tokio"] +wasm = ["wasm-bindgen-futures", "serde-wasm-bindgen", "web-sys", "wasm-bindgen", "js-sys", "wasmer"] + +[dependencies] +anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +wasm-bindgen = { version = "0.2", optional = true } +js-sys = { version = "0.3", optional = true } +wasm-bindgen-futures = { version = "0.4.37", optional = true } +serde-wasm-bindgen = { version = "0.5.0", optional = true } +web-sys = { version = "0.3", features = ['console'], optional = true } + +reqwest = { version = "0.11", features = ["json", "stream"], optional = true} +futures = { version = "0.3.29", optional = true } +bytes = { version = "1.5.0", optional = true } +tokio = { version = "1.34.0", optional = true } +wasmer = { path = "../wasmer", optional = true } + + +[dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/crates/oai/src/lib.rs b/crates/oai/src/lib.rs new file mode 100644 index 0000000..1762113 --- /dev/null +++ b/crates/oai/src/lib.rs @@ -0,0 +1,17 @@ +///< Client for interacting with the OpenAI API. +pub mod models; +pub mod utils; + +#[cfg(feature = "wasm")] +pub mod foreign { + use wasm_bindgen::prelude::wasm_bindgen; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(typescript_type = "Record")] + pub type IModels; + + #[wasm_bindgen(typescript_type = "Array")] + pub type IMessages; + } +} diff --git a/crates/neatcoder/src/openai/assistant/message.rs b/crates/oai/src/models/chat/client.rs similarity index 100% rename from crates/neatcoder/src/openai/assistant/message.rs rename to crates/oai/src/models/chat/client.rs diff --git a/crates/oai/src/models/chat/mod.rs b/crates/oai/src/models/chat/mod.rs new file mode 100644 index 0000000..eaaf55b --- /dev/null +++ b/crates/oai/src/models/chat/mod.rs @@ -0,0 +1,3 @@ +pub mod params; +pub mod request; +pub mod response; diff --git a/crates/neatcoder/src/openai/params.rs b/crates/oai/src/models/chat/params.rs similarity index 54% rename from crates/neatcoder/src/openai/params.rs rename to crates/oai/src/models/chat/params.rs index 3daf3f2..1b83ace 100644 --- a/crates/neatcoder/src/openai/params.rs +++ b/crates/oai/src/models/chat/params.rs @@ -1,40 +1,15 @@ use anyhow::Result; -use serde::{ - de::{self, Visitor}, - Deserialize, Deserializer, Serialize, -}; -use std::{collections::HashMap, fmt}; -use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; +use serde::Serialize; +use std::collections::HashMap; use crate::{ - openai::utils::{BoundedFloat, Range100s}, - utils::jsvalue_to_hmap, - JsError, + models::Models, + utils::{Bounded, BoundedFloat, Scale01, Scale100s, Scale22}, }; -use super::utils::{Bounded, Scale01, Scale100s, Scale22}; - -#[wasm_bindgen] -#[derive(Debug, Serialize, Clone, Copy)] -pub enum OpenAIModels { - Gpt432k, - Gpt4, - Gpt35Turbo, - Gpt35Turbo16k, - Gpt35Turbo1106, - Gpt41106Preview, -} - -impl Default for OpenAIModels { - fn default() -> Self { - OpenAIModels::Gpt35Turbo16k - } -} - -#[wasm_bindgen] #[derive(Debug, Serialize, Clone)] -pub struct OpenAIParams { - pub model: OpenAIModels, +pub struct ChatParams { + pub model: Models, // TODO: THIS SHOULD BE Scale02 /// Temperature is used to control the randomness or creativity /// of the model's output. Temperature is a parameter that affects @@ -55,7 +30,7 @@ pub struct OpenAIParams { /// narrower range of high-probability words. This leads to /// more focused and deterministic output, with fewer alternatives /// and reduced randomness. - pub(crate) top_p: Option, // TODO: Add getter + pub top_p: Option, // TODO: Add getter /// The frequency penalty parameter helps reduce the repetition of words /// or sentences within the generated text. It is a float value ranging /// from -2.0 to 2.0, which is subtracted to the logarithmic probability of a @@ -65,7 +40,7 @@ pub struct OpenAIParams { /// /// In the official documentation: /// https://platform.openai.com/docs/api-reference/chat/create#chat/create-frequency_penalty - pub(crate) frequency_penalty: Option, // TODO: Add getter + pub frequency_penalty: Option, // TODO: Add getter /// The presence penalty parameter stears how the model penalizes new tokens /// based on whether they have appeared (hence presense) in the text so far. /// @@ -74,7 +49,7 @@ pub struct OpenAIParams { /// /// In the official documentation: /// https://platform.openai.com/docs/api-reference/chat/create#chat/create-presence_penalty - pub(crate) presence_penalty: Option, // TODO: Add getter + pub presence_penalty: Option, // TODO: Add getter /// How many chat completion choices to generate for each input message. pub n: Option, /// Whether to stream back partial progress. If set, tokens will be sent as @@ -90,18 +65,16 @@ pub struct OpenAIParams { /// between -1 and 1 should decrease or increase likelihood of selection; /// values like -100 or 100 should result in a ban or exclusive selection /// of the relevant token. - pub(crate) logit_bias: HashMap, // TODO: Add getter + pub logit_bias: HashMap, // TODO: Add getter /// A unique identifier representing the end-user, which can help OpenAI /// to monitor and detect abuse. You can read more at: /// https://platform.openai.com/docs/guides/safety-best-practices/end-user-ids - pub(crate) user: Option, + pub user: Option, } -#[wasm_bindgen] -impl OpenAIParams { - #[wasm_bindgen(constructor)] +impl ChatParams { pub fn new( - model: OpenAIModels, + model: Models, temperature: Option, max_tokens: Option, top_p: Option, @@ -109,36 +82,28 @@ impl OpenAIParams { presence_penalty: Option, n: Option, stream: bool, - logit_bias: JsValue, + logit_bias: HashMap, user: Option, - ) -> Result { + ) -> Result { let top_p = match top_p { - Some(top_p) => Some( - BoundedFloat::new(top_p) - .map_err(|e| JsError::from_str(&e.to_string()))?, - ), + Some(top_p) => Some(BoundedFloat::new(top_p)?), None => None, }; let frequency_penalty = match frequency_penalty { - Some(frequency_penalty) => Some( - BoundedFloat::new(frequency_penalty) - .map_err(|e| JsError::from_str(&e.to_string()))?, - ), + Some(frequency_penalty) => { + Some(BoundedFloat::new(frequency_penalty)?) + } None => None, }; let presence_penalty = match presence_penalty { - Some(presence_penalty) => Some( - BoundedFloat::new(presence_penalty) - .map_err(|e| JsError::from_str(&e.to_string()))?, - ), + Some(presence_penalty) => { + Some(BoundedFloat::new(presence_penalty)?) + } None => None, }; - let logit_bias = - jsvalue_to_hmap::>(logit_bias)?; - Ok(Self { model, temperature, @@ -153,7 +118,7 @@ impl OpenAIParams { }) } - pub fn empty(model: OpenAIModels) -> Self { + pub fn empty(model: Models) -> Self { Self { model, temperature: None, @@ -170,19 +135,27 @@ impl OpenAIParams { // === Setter methods with chaining === - #[wasm_bindgen(js_name = topP)] + pub fn logit_bias( + mut self, + logit_bias: HashMap, + ) -> Result { + self.logit_bias = logit_bias; + Ok(self) + } +} + +impl ChatParams { + // === Setter methods with chaining === pub fn top_p(mut self, top_p: f64) -> Self { self.top_p = Some(Scale01::new(top_p).expect("Invalid top_p value")); self } - #[wasm_bindgen(js_name = maxTokens)] pub fn max_tokens(mut self, max_tokens: u64) -> Self { self.max_tokens = Some(max_tokens); self } - #[wasm_bindgen(js_name = frequencyPenalty)] pub fn frequency_penalty(mut self, frequency_penalty: f64) -> Self { self.frequency_penalty = Some( Scale22::new(frequency_penalty).expect("Invalid frequency penalty"), @@ -190,7 +163,6 @@ impl OpenAIParams { self } - #[wasm_bindgen(js_name = presencePenalty)] pub fn presence_penalty(mut self, presence_penalty: f64) -> Self { self.presence_penalty = Some( Scale22::new(presence_penalty).expect("Invalid presence penalty"), @@ -198,106 +170,148 @@ impl OpenAIParams { self } - #[wasm_bindgen(js_name = logicBias)] - pub fn logit_bias( - mut self, - logit_bias: JsValue, - ) -> Result { - let mut logit_bias = jsvalue_to_hmap::(logit_bias)?; - - let logit_bias = logit_bias - .drain() - .map(|(key, val)| Ok((key, Scale100s::new(val)?))) - .collect::>>() - .map_err(|e| JsError::from_str(&e.to_string()))?; - - self.logit_bias = logit_bias; - Ok(self) - } - pub fn user(mut self, user: String) -> Self { self.user = Some(user); self } + + pub fn default_with_model(model: Models) -> Self { + Self { + model, + temperature: None, + max_tokens: None, + top_p: None, + frequency_penalty: None, + presence_penalty: None, + n: None, + stream: false, + logit_bias: HashMap::new(), + user: None, + } + } } -impl OpenAIModels { - pub fn new(model: String) -> Self { - let model = match model.as_str() { - "gpt-4-32k" => OpenAIModels::Gpt432k, - "gpt-4" => OpenAIModels::Gpt4, - "gpt-3.5-turbo" => OpenAIModels::Gpt35Turbo, - "gpt-3.5-turbo-16k" => OpenAIModels::Gpt35Turbo16k, - "gpt-3.5-turbo-1106" => OpenAIModels::Gpt35Turbo1106, - "gpt-4-1106-preview" => OpenAIModels::Gpt41106Preview, - _ => panic!("Invalid model {}", model), - }; +// ==== WASM ==== - model - } +#[cfg(feature = "wasm")] +pub mod wasm { + use std::ops::{Deref, DerefMut}; + + use super::*; + + use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + use wasmer::{jsvalue_to_hmap, JsError}; - pub fn as_string(&self) -> String { - match self { - OpenAIModels::Gpt432k => String::from("gpt-4-32k"), - OpenAIModels::Gpt4 => String::from("gpt-4"), - OpenAIModels::Gpt35Turbo => String::from("gpt-3.5-turbo"), - OpenAIModels::Gpt35Turbo16k => String::from("gpt-3.5-turbo-16k"), - OpenAIModels::Gpt35Turbo1106 => String::from("gpt-3.5-turbo-1106"), - OpenAIModels::Gpt41106Preview => String::from("gpt-4-1106-preview"), + #[wasm_bindgen(js_name = "ChatParams")] + pub struct ChatParamsWasm(ChatParams); + + #[wasm_bindgen] + impl ChatParamsWasm { + #[wasm_bindgen(constructor)] + pub fn new( + model: Models, + temperature: Option, + max_tokens: Option, + top_p: Option, + frequency_penalty: Option, + presence_penalty: Option, + n: Option, + stream: bool, + logit_bias: JsValue, + user: Option, + ) -> Result { + let mut logit_bias = jsvalue_to_hmap(logit_bias)?; + + let params = ChatParamsWasm( + ChatParams::new( + model, + temperature, + max_tokens, + top_p, + frequency_penalty, + presence_penalty, + n, + stream, + logit_bias, + user, + ) + .map_err(|e| JsError::from_str(&e.to_string()))?, + ); + + Ok(params) } - } -} -impl<'de> Deserialize<'de> for OpenAIModels { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct OpenAIModelsVisitor; + #[wasm_bindgen] + pub fn empty(model: Models) -> Self { + ChatParamsWasm(ChatParams::empty(model)) + } - impl<'de> Visitor<'de> for OpenAIModelsVisitor { - type Value = OpenAIModels; + #[wasm_bindgen(js_name = logitBias)] + pub fn logit_bias( + mut self, + logit_bias: JsValue, + ) -> Result { + let mut logit_bias = jsvalue_to_hmap(logit_bias)?; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string representing an OpenAI model") - } + let logit_bias = logit_bias + .drain() + .map(|(key, val)| Ok((key, Scale100s::new(val)?))) + .collect::>>() + .map_err(|e| JsError::from_str(&e.to_string()))?; - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - match value { - "gpt-4-32k" => Ok(OpenAIModels::Gpt432k), - "gpt-4" => Ok(OpenAIModels::Gpt4), - "gpt-3.5-turbo" => Ok(OpenAIModels::Gpt35Turbo), - "gpt-3.5-turbo-16k" => Ok(OpenAIModels::Gpt35Turbo16k), - "gpt-3.5-turbo-1106" => Ok(OpenAIModels::Gpt35Turbo1106), - "gpt-4-1106-preview" => Ok(OpenAIModels::Gpt41106Preview), - _ => Err(E::custom(format!( - "unexpected OpenAI model: {}", - value - ))), - } - } + self.logit_bias = logit_bias; + Ok(self) + } + + #[wasm_bindgen(js_name = topP)] + pub fn top_p(mut self, top_p: f64) -> Self { + self.top_p = + Some(Scale01::new(top_p).expect("Invalid top_p value")); + self + } + + #[wasm_bindgen(js_name = maxTokens)] + pub fn max_tokens(mut self, max_tokens: u64) -> Self { + self.max_tokens = Some(max_tokens); + self + } + + #[wasm_bindgen(js_name = frequencyPenalty)] + pub fn frequency_penalty(mut self, frequency_penalty: f64) -> Self { + self.frequency_penalty = Some( + Scale22::new(frequency_penalty) + .expect("Invalid frequency penalty"), + ); + self } - deserializer.deserialize_str(OpenAIModelsVisitor) + #[wasm_bindgen(js_name = presencePenalty)] + pub fn presence_penalty(mut self, presence_penalty: f64) -> Self { + self.presence_penalty = Some( + Scale22::new(presence_penalty) + .expect("Invalid presence penalty"), + ); + self + } } -} -impl Default for OpenAIParams { - fn default() -> Self { - Self { - model: OpenAIModels::Gpt35Turbo, - temperature: None, - max_tokens: None, - top_p: None, - frequency_penalty: None, - presence_penalty: None, - n: None, - stream: false, - logit_bias: HashMap::new(), - user: None, + impl AsRef for ChatParamsWasm { + fn as_ref(&self) -> &ChatParams { + &self.0 + } + } + + impl Deref for ChatParamsWasm { + type Target = ChatParams; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl DerefMut for ChatParamsWasm { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } } diff --git a/crates/oai/src/models/chat/request.rs b/crates/oai/src/models/chat/request.rs new file mode 100644 index 0000000..c26c61d --- /dev/null +++ b/crates/oai/src/models/chat/request.rs @@ -0,0 +1,386 @@ +use anyhow::{anyhow, Result}; +use serde_json::{json, Value}; +use std::ops::Deref; + +use crate::models::{ + chat::{params::ChatParams, response::ChatResponse}, + message::Message, +}; + +#[cfg(not(feature = "wasm"))] +pub mod native { + use super::*; + use bytes::Bytes; + use futures::Stream; + use std::{ops::Deref, time::Duration}; + + use reqwest::{header::HeaderMap, Client}; + + pub struct ChatClient { + pub(crate) headers: HeaderMap, + } + + impl ChatClient { + pub async fn chat( + &self, + job: impl Deref, + msgs: &[&Message], + funcs: &[&String], + stop_seq: &[String], + ) -> Result { + let client = Client::new(); + + // fill in your own data as needed + let req_body = + super::request_body(job, msgs, funcs, stop_seq, false)?; + println!("[DEBUG] Sending reqeust to OpenAI..."); + + let res = client + .post("https://api.openai.com/v1/chat/completions") + .headers(self.headers.clone()) + .json(&req_body) + .send() + .await?; + + println!("[DEBUG] Got response....."); + let status = res.status(); + + if !status.is_success() { + return Err(anyhow!("Failed with status: {}", status)); + } + + // let body = res.text().await?; + let body = res.json::().await?; + let api_response = serde_json::from_value(body)?; + + Ok(api_response) + } + + pub async fn chat_stream( + job: &ChatParams, + msgs: &[&Message], + funcs: &[&String], + stop_seq: &[String], + ) -> Result>> { + let client = Client::new(); + + // fill in your own data as needed + let req_body = request_body(job, msgs, funcs, stop_seq, true)?; + + let mut retries = 3; // Number of retries + loop { + println!("[DEBUG] Sending request to OpenAI..."); + + let res = tokio::time::timeout( + Duration::from_secs(5), + client + .post("https://api.openai.com/v1/chat/completions") + .headers(headers.clone()) + .json(&req_body) + .send(), + ) + .await?; + + match res { + Ok(response) => { + println!("[DEBUG] Got response....."); + let stream = response.bytes_stream(); + + return Ok(stream); + } + Err(e) => { + retries -= 1; + if retries == 0 { + return Err(anyhow!( + "Failed after maximum retries: {:?}", + e + )); + } + + println!("[DEBUG] Request failed, retrying..."); + } + } + } + } + } +} + +#[cfg(feature = "wasm")] +pub mod wasm { + use crate::{ + foreign::IMessages, + models::{ + chat::params::wasm::ChatParamsWasm, message::wasm::MessageWasm, + }, + }; + + use super::*; + use js_sys::{Function, Promise}; + use serde_wasm_bindgen::from_value; + use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; + use wasm_bindgen_futures::JsFuture; + use wasmer::{log, JsError, WasmType}; + + pub async fn chat( + request_callback: &Function, + ai_params: &ChatParamsWasm, + msgs: &[&MessageWasm], + funcs: &[&String], + stop_seq: &[String], + ) -> Result { + log("[DEBUG] Getting Chat Raw..."); + let chat = chat_raw(request_callback, ai_params, msgs, funcs, stop_seq) + .await?; + + log("[DEBUG] Got answer."); + + let answer = chat + .choices + .first() + .ok_or_else(|| anyhow!("LLM Respose seems to be empty :("))? + .message + .content + .as_str(); + + Ok(String::from(answer)) + } + + pub async fn chat_raw( + request_callback: &Function, + ai_params: &ChatParamsWasm, + msgs: &[&MessageWasm], + funcs: &[&String], + stop_seq: &[String], + ) -> Result { + let msgs: Vec<&Message> = + msgs.iter().map(|&m_wasm| m_wasm.deref()).collect(); + + let msg_slice: &[&Message] = &msgs; + + let req_body = + request_body(ai_params.deref(), msg_slice, funcs, stop_seq, false)?; + + let body_json = serde_json::to_string(&req_body)?; + + log("[DEBUG] Getting promise..."); + let js_promise: Promise = request_callback + .call1(&JsValue::NULL, &JsValue::from_str(&body_json)) + .map_err(|e| anyhow!("Error performing request callback: {:?}", e))? + .dyn_into() + .map_err(|e| { + anyhow!("Error processing request callback promise: {:?}", e) + })?; + log("[INFO] Prompting the LLM..."); + let res_js_value: JsValue = + JsFuture::from(js_promise).await.map_err(|e| { + anyhow!("Error processing request callback result: {:?}", e) + })?; + + log("[INFO] Receive response from LLM.."); + log(&format!("LLM response body: {:?}", res_js_value)); + + let body: Result = from_value(res_js_value); + + match body { + Ok(body) => Ok(body), + Err(e) => { + Err(anyhow!("Could not convert JsValue to Response: {:?}", e)) + } + } + } + + #[wasm_bindgen(js_name = requestBody)] + pub fn request_body_wasm( + msgs: IMessages, + job: ChatParamsWasm, + stream: bool, + ) -> Result { + let msgs: Vec = Vec::from_extern(msgs).map_err(|e| { + JsValue::from_str(&format!( + "Failed to convert msgs to native Wasm type: {:?}", + e + )) + })?; + + let mut data = json!({ + "model": job.model.as_str(), + "messages": msgs, + }); + + if stream { + data["stream"] = serde_json::Value::Bool(true); + } + + let serialized_str = serde_json::to_string(&data).map_err(|e| { + JsValue::from_str(&format!( + "Failed to serialize to string: {:?}", + e + )) + })?; + + Ok(JsValue::from_str(&serialized_str)) + } +} + +pub fn request_stream( + ai_params: impl Deref, + msgs: &[&Message], + funcs: &[&String], + stop_seq: &[String], +) -> Result { + let req_body = request_body(ai_params, msgs, funcs, stop_seq, true)?; + + let body_json = serde_json::to_string(&req_body)?; + + Ok(body_json) +} + +pub fn request_body( + job: impl Deref, + msgs: &[&Message], + funcs: &[&String], // TODO: Add to ChatParams + stop_seq: &[String], // TODO: Add to ChatParams + stream: bool, +) -> Result { + let mut data = json!({ + "model": job.model.as_str(), + "messages": msgs, + // "stop": self.stop, + }); + + if let Some(temperature) = job.temperature { + data["temperature"] = serde_json::to_value(temperature)?; + } + + if let Some(top_p) = job.top_p { + data["top_p"] = serde_json::to_value(top_p)?; + } + + if !funcs.is_empty() { + data["functions"] = serde_json::to_value(funcs)?; + } + + if !stop_seq.is_empty() { + if stop_seq.len() > 4 { + return Err(anyhow!( + "Maximum limit of stop sequences reached. {} out of 4", + stop_seq.len() + )); + }; + + data["stop"] = serde_json::to_value(stop_seq)?; + } + + if let Some(user) = &job.user { + data["user"] = serde_json::to_value(user)?; + } + + if !job.logit_bias.is_empty() { + data["logit_bias"] = serde_json::to_value(&job.logit_bias)?; + } + + if let Some(frequency_penalty) = &job.frequency_penalty { + data["frequency_penalty"] = serde_json::to_value(frequency_penalty)?; + } + + if let Some(presence_penalty) = &job.presence_penalty { + data["presence_penalty"] = serde_json::to_value(presence_penalty)?; + } + + if let Some(max_tokens) = &job.max_tokens { + data["max_tokens"] = serde_json::to_value(max_tokens)?; + } + + if let Some(n) = &job.n { + data["n"] = serde_json::to_value(n)?; + } + + if stream { + data["stream"] = serde_json::Value::Bool(true); + } + + Ok(data) +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + + #[cfg(feature = "wasm")] + use serde_wasm_bindgen::{from_value, to_value}; + #[cfg(feature = "wasm")] + use wasm_bindgen_test::wasm_bindgen_test; + + #[cfg(feature = "wasm")] + #[wasm_bindgen_test] + fn parse_js_callback_response() -> Result<()> { + use crate::models::chat::response::ChatResponse; + + let raw_response = r#" + { + "id":"chatcmpl-7wRVa4TZBNplfgGR6vfm6mT3j7uFT", + "object":"chat.completion", + "created":1694163122, + "model":"gpt-3.5-turbo-16k-0613", + "choices":[ + { + "index":0, + "message":{ + "role":"assistant", + "content":"```json\n{\n \"src\": {\n \"main.rs\": \"The main entry point of the Rust application\",\n \"config.rs\": \"A module for loading and managing configuration\",\n \"database.rs\": \"A module for handling database connection and queries\",\n \"models.rs\": \"A module defining the data models used in the application\",\n \"handlers\": {\n \"mod.rs\": \"A module for defining request handlers\",\n \"product_handler.rs\": \"A module for handling product-related requests\",\n \"order_handler.rs\": \"A module for handling order-related requests\",\n \"cart_handler.rs\": \"A module for handling shopping cart-related requests\"\n },\n \"middlewares\": {\n \"mod.rs\": \"A module for defining middlewares\",\n \"authentication.rs\": \"A middleware for handling authentication\",\n \"authorization.rs\": \"A middleware for handling authorization\"\n },\n \"routes\": {\n \"mod.rs\": \"A module for defining API routes\",\n \"product_routes.rs\": \"A module for defining product-related routes\",\n \"order_routes.rs\": \"A module for defining order-related routes\",\n \"cart_routes.rs\": \"A module for defining shopping cart-related routes\"\n },\n \"errors.rs\": \"A module defining custom error types and error handling\",\n \"util.rs\": \"A module containing utility functions used throughout the application\"\n }\n}\n```" + }, + "finish_reason":"stop" + } + ], + "usage":{ + "prompt_tokens":112, + "completion_tokens":282, + "total_tokens":394 + } + }"#; + + let json_value: ChatResponse = serde_json::from_str(raw_response)?; + let js_value = to_value(&json_value).unwrap(); + + let body: Result = from_value(js_value); + + if let Err(e) = body { + panic!("Upsie: {:?}", e); + } + + Ok(()) + } + + #[cfg(not(feature = "wasm"))] + #[test] + fn parse_response() -> Result<()> { + use crate::models::chat::response::ChatResponse; + + let raw_response = r#" + { + "id":"chatcmpl-7wRVa4TZBNplfgGR6vfm6mT3j7uFT", + "object":"chat.completion", + "created":1694163122, + "model":"gpt-3.5-turbo-16k-0613", + "choices":[ + { + "index":0, + "message":{ + "role":"assistant", + "content":"```json\n{\n \"src\": {\n \"main.rs\": \"The main entry point of the Rust application\",\n \"config.rs\": \"A module for loading and managing configuration\",\n \"database.rs\": \"A module for handling database connection and queries\",\n \"models.rs\": \"A module defining the data models used in the application\",\n \"handlers\": {\n \"mod.rs\": \"A module for defining request handlers\",\n \"product_handler.rs\": \"A module for handling product-related requests\",\n \"order_handler.rs\": \"A module for handling order-related requests\",\n \"cart_handler.rs\": \"A module for handling shopping cart-related requests\"\n },\n \"middlewares\": {\n \"mod.rs\": \"A module for defining middlewares\",\n \"authentication.rs\": \"A middleware for handling authentication\",\n \"authorization.rs\": \"A middleware for handling authorization\"\n },\n \"routes\": {\n \"mod.rs\": \"A module for defining API routes\",\n \"product_routes.rs\": \"A module for defining product-related routes\",\n \"order_routes.rs\": \"A module for defining order-related routes\",\n \"cart_routes.rs\": \"A module for defining shopping cart-related routes\"\n },\n \"errors.rs\": \"A module defining custom error types and error handling\",\n \"util.rs\": \"A module containing utility functions used throughout the application\"\n }\n}\n```" + }, + "finish_reason":"stop" + } + ], + "usage":{ + "prompt_tokens":112, + "completion_tokens":282, + "total_tokens":394 + } + }"#; + + let body: ChatResponse = serde_json::from_str(raw_response).unwrap(); + + Ok(()) + } +} diff --git a/crates/neatcoder/src/openai/response.rs b/crates/oai/src/models/chat/response.rs similarity index 92% rename from crates/neatcoder/src/openai/response.rs rename to crates/oai/src/models/chat/response.rs index 2325915..b058547 100644 --- a/crates/neatcoder/src/openai/response.rs +++ b/crates/oai/src/models/chat/response.rs @@ -1,11 +1,11 @@ use serde::{Deserialize, Serialize}; -use super::msg::OpenAIMsg; +use crate::models::message::Message; #[derive(Serialize, Deserialize, Debug)] pub struct ApiResponse { pub headers: ResponseHeaders, - pub body: ResponseBody, + pub body: ChatResponse, } #[derive(Serialize, Deserialize, Debug)] @@ -35,7 +35,7 @@ pub struct ResponseHeaders { } #[derive(Serialize, Deserialize, Debug)] -pub struct ResponseBody { +pub struct ChatResponse { pub id: String, pub object: String, pub created: i64, @@ -47,7 +47,7 @@ pub struct ResponseBody { #[derive(Serialize, Deserialize, Debug)] pub struct Choice { pub index: i32, - pub message: OpenAIMsg, + pub message: Message, pub finish_reason: String, } diff --git a/crates/oai/src/models/message.rs b/crates/oai/src/models/message.rs new file mode 100644 index 0000000..d4fbdb9 --- /dev/null +++ b/crates/oai/src/models/message.rs @@ -0,0 +1,111 @@ +use serde::{Deserialize, Serialize}; + +use super::role::Role; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Message { + pub role: Role, + pub content: String, +} + +impl Message { + pub fn user(content: &str) -> Self { + Self { + role: Role::User, + content: String::from(content), + } + } + + pub fn system(content: &str) -> Self { + Self { + role: Role::System, + content: String::from(content), + } + } + + pub fn assistant(content: &str) -> Self { + Self { + role: Role::Assistant, + content: String::from(content), + } + } +} + +// ===== WASM ===== + +#[cfg(feature = "wasm")] +pub mod wasm { + use std::ops::{Deref, DerefMut}; + + use js_sys::JsString; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use wasm_bindgen::prelude::wasm_bindgen; + + use crate::models::role::Role; + + use super::Message; + + #[wasm_bindgen(js_name = "Message")] + #[derive(Debug, Clone)] + pub struct MessageWasm(Message); + + #[wasm_bindgen] + impl MessageWasm { + #[wasm_bindgen(constructor)] + pub fn new(role: Role, content: String) -> Self { + Self(Message { role, content }) + } + + #[wasm_bindgen(getter)] + pub fn role(&self) -> JsString { + self.role.as_str().to_string().into() + } + + #[wasm_bindgen(getter)] + pub fn content(&self) -> JsString { + self.content.clone().into() + } + } + + impl AsRef for MessageWasm { + fn as_ref(&self) -> &Message { + &self.0 + } + } + + impl Deref for MessageWasm { + type Target = Message; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl DerefMut for MessageWasm { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + // Implement Serialize for MessageWasm by delegating to Message. + impl Serialize for MessageWasm { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // Serialize the inner Message. + self.0.serialize(serializer) + } + } + + // Implement Deserialize for MessageWasm by delegating to Message. + impl<'de> Deserialize<'de> for MessageWasm { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Deserialize as Message and wrap inside MessageWasm. + Message::deserialize(deserializer).map(MessageWasm) + } + } +} diff --git a/crates/oai/src/models/mod.rs b/crates/oai/src/models/mod.rs new file mode 100644 index 0000000..4645a5d --- /dev/null +++ b/crates/oai/src/models/mod.rs @@ -0,0 +1,43 @@ +pub mod chat; +pub mod message; +pub mod role; + +use ::anyhow::Result; +use serde::{Deserialize, Serialize, Serializer}; + +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum Models { + #[serde(rename = "gpt-4-32k")] + Gpt432k, + #[serde(rename = "gpt-4")] + Gpt4, + #[serde(rename = "gpt-3.5-turbo")] + Gpt35Turbo, + #[serde(rename = "gpt-3.5-turbo-16k")] + Gpt35Turbo16k, + #[serde(rename = "gpt-3.5-turbo-1106")] + Gpt35Turbo1106, + #[serde(rename = "gpt-4-1106-preview")] + Gpt41106Preview, +} + +impl Models { + pub fn from_str(file_purpose: &str) -> Result { + serde_json::from_str(file_purpose).map_err(Into::into) + } + + pub fn as_str(&self) -> &str { + match self { + Models::Gpt432k => "gpt-4-32k", + Models::Gpt4 => "gpt-4", + Models::Gpt35Turbo => "gpt-3.5-turbo", + Models::Gpt35Turbo16k => "gpt-3.5-turbo-16k", + Models::Gpt35Turbo1106 => "gpt-3.5-turbo-1106", + Models::Gpt41106Preview => "gpt-4-1106-preview", + } + } +} diff --git a/crates/oai/src/models/role.rs b/crates/oai/src/models/role.rs new file mode 100644 index 0000000..15d8a86 --- /dev/null +++ b/crates/oai/src/models/role.rs @@ -0,0 +1,63 @@ +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[derive(Debug, Clone, Copy)] +pub enum Role { + System, + User, + Assistant, +} + +impl Serialize for Role { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Role::System => serializer.serialize_str("system"), + Role::User => serializer.serialize_str("user"), + Role::Assistant => serializer.serialize_str("assistant"), + } + } +} + +impl<'de> Deserialize<'de> for Role { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + match s.as_str() { + "system" => Ok(Role::System), + "user" => Ok(Role::User), + "assistant" => Ok(Role::Assistant), + _ => panic!("Invalid variant `{:?}`", s.as_str()), + } + } +} + +impl Role { + pub fn new(role: &str) -> Result { + let role = match role { + "system" => Role::System, + "user" => Role::User, + "assistant" => Role::Assistant, + _ => return Err(anyhow!(format!("Invalid role {}", role))), + }; + + Ok(role) + } + + pub fn as_str(&self) -> &str { + match self { + Role::System => "system", + Role::User => "user", + Role::Assistant => "assistant", + } + } +} diff --git a/crates/neatcoder/src/openai/utils.rs b/crates/oai/src/utils.rs similarity index 100% rename from crates/neatcoder/src/openai/utils.rs rename to crates/oai/src/utils.rs diff --git a/crates/wasmer/Cargo.toml b/crates/wasmer/Cargo.toml new file mode 100644 index 0000000..d11b964 --- /dev/null +++ b/crates/wasmer/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "wasmer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +path = "src/lib.rs" +crate-type = ["lib", "cdylib"] + +[dependencies] +anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +wasm-bindgen = { version = "0.2" } +js-sys = { version = "0.3" } +wasm-bindgen-futures = { version = "0.4.37" } +serde-wasm-bindgen = { version = "0.5.0" } +web-sys = { version = "0.3", features = ['console'] } +chrono = {version = "0.4", features = ["serde"]} diff --git a/crates/wasmer/src/lib.rs b/crates/wasmer/src/lib.rs new file mode 100644 index 0000000..04e3971 --- /dev/null +++ b/crates/wasmer/src/lib.rs @@ -0,0 +1,377 @@ +use chrono::{DateTime, Utc}; +use js_sys::{Date as IDate, Reflect}; +use serde::{de::DeserializeOwned, Serialize}; +use serde_wasm_bindgen::to_value; +use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::hash::Hash; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::console; + +/// Type alias for JavaScript errors represented as JsValue +pub type JsError = JsValue; + +// TODO: This function is on life support and it will be removed in the next serde generalisatoin cycles. +pub fn jsvalue_to_hmap( + value: JsValue, +) -> Result, JsError> { + serde_wasm_bindgen::from_value(value) + .map_err(|e| JsError::from_str(&e.to_string())) +} + +pub fn log(msg: &str) { + console::log_1(&JsValue::from_str(msg)); +} + +pub fn log_err(msg: &str) { + console::error_1(&JsValue::from_str(&msg)); +} + +/// Trait to represent a type conversion between Rust and JavaScript types. +/// +/// This trait provides methods to convert between a Rust type and a corresponding +/// JavaScript type which can be cast from JsValue. +pub trait WasmType { + /// The type used in Rust representation + type RustType; + + /// Converts a Rust type into an external (JavaScript) type + /// + /// # Parameters + /// - `rust_type`: The Rust type to be converted. + /// + /// # Returns + /// - A Result containing the JavaScript type or an error. + fn to_extern(rust_type: Self::RustType) -> Result; + + /// Converts an external (JavaScript) type into a Rust type + /// + /// # Parameters + /// - `extern_type`: The JavaScript type to be converted. + /// + /// # Returns + /// - A Result containing the Rust type or an error. + fn from_extern(extern_type: ExternType) -> Result; +} + +/// Implementation of WasmType for BTreeMap with methods for converting between Rust and JavaScript types. +impl< + K: DeserializeOwned + Ord + Into + Clone, + V: Into + Clone, + ExternType: JsCast, + > WasmType for BTreeMap +where + V: DeserializeOwned, +{ + type RustType = BTreeMap; + + // Method to convert a BTreeMap to a JavaScript object representation + fn to_extern(rust_type: Self::RustType) -> Result { + // Create a new JavaScript object + let js_object = js_sys::Object::new(); + + // Set properties on the JavaScript object from the BTreeMap + for (key, value) in rust_type.iter() { + Reflect::set( + &js_object, + &key.clone().into(), + &value.clone().into(), + ) + .map_err(|e| { + JsValue::from_str(&format!( + "Failed to set property on JsValue: {:?}", + e + )) + })?; + } + + // Try to cast the JsValue to ExternType + // let ischemas: ExternType = js_object.dyn_into().map_err(|e| { + // JsValue::from_str(&format!( + // "Failed to cast Object `{:?}` to ExternType. Error: {:?}", + // js_object, e + // )) + // })?; + + // Unchecked here can potentially lead to problems down the line. + // However somehow `js_object.dyn_into()` messes up the casting.. + let extern_type = js_object.unchecked_into::(); + + Ok(extern_type) + } + + // Method to convert a JavaScript object representation to a BTreeMap + fn from_extern(extern_type: ExternType) -> Result { + let type_name = std::any::type_name::(); + + let js_value = extern_type.dyn_into::().map_err(|_| { + JsValue::from_str(&format!( + "Failed to convert {} to JsValue", + type_name + )) + })?; + + serde_wasm_bindgen::from_value(js_value).map_err(|e| { + let error_msg = format!( + "Failed to convert {} JsValue to BTreeMap: {:?}", + type_name, + std::any::type_name::(), + e, + ); + log_err(&error_msg); + JsValue::from_str(&error_msg) + }) + } +} + +/// Implementation of WasmType for VecDeque with methods for converting between Rust and JavaScript types. +impl< + V: DeserializeOwned + Serialize + Into + Clone, + ExternType: JsCast, + > WasmType for VecDeque +{ + type RustType = VecDeque; + + // Method to convert a VecDeque to a JavaScript object representation + fn to_extern(rust_type: Self::RustType) -> Result { + // Create a new JavaScript array + let js_array = js_sys::Array::new(); + + // Add elements from the VecDeque to the JavaScript array + // for value in rust_type.iter() { + // js_array.push(&value.clone().into()); + // } + + // Add elements from the VecDeque to the JavaScript object as array + for (index, value) in rust_type.iter().enumerate() { + let js_index = to_value(&(index as u32)).map_err(|e| { + JsError::from(JsValue::from_str(&format!( + "Failed to convert index to JsValue: {:?}", + e + ))) + })?; + + let js_value = to_value(&value.clone()).map_err(|e| { + JsError::from(JsValue::from_str(&format!( + "Failed to convert value to JsValue: {:?}", + e + ))) + })?; + + Reflect::set(&js_array, &js_index, &js_value).map_err(|e| { + JsValue::from_str(&format!( + "Failed to set property on JsValue: {:?}", + e + )) + })?; + } + + // Attempt to cast the JsValue (Array) to the ExternType + // We use unchecked_into here for the reasons mentioned in the BTreeMap implementation + let extern_type = js_array.unchecked_into::(); + + Ok(extern_type) + } + + // Method to convert a JavaScript object representation to a VecDeque + fn from_extern(extern_type: ExternType) -> Result { + let type_name = std::any::type_name::(); + + let js_value = extern_type.dyn_into::().map_err(|_| { + JsValue::from_str(&format!( + "Failed to convert {} to JsValue", + type_name + )) + })?; + + serde_wasm_bindgen::from_value(js_value).map_err(|e| { + let error_msg = format!( + "Failed to convert {} JsValue to VecDeque<{}>: {:?}", + type_name, + std::any::type_name::(), + e, + ); + log_err(&error_msg); + JsValue::from_str(&error_msg) + }) + } +} + +// TODO: dedup implementation of Vec/VecDeque +/// Implementation of WasmType for Vec with methods for converting between Rust and JavaScript types. +impl< + V: DeserializeOwned + Serialize + Into + Clone, + ExternType: JsCast, + > WasmType for Vec +{ + type RustType = Vec; + + // Add elements from the Vec to the JavaScript object as array + fn to_extern(rust_type: Self::RustType) -> Result { + // Create a new JavaScript array + let js_array = js_sys::Array::new(); + + // Add elements from the Vec to the JavaScript array + // for value in rust_type.iter() { + // js_array.push(&value.clone().into()); + // } + + // Add elements from the Vec to the JavaScript object as array + for (index, value) in rust_type.iter().enumerate() { + let js_index = to_value(&(index as u32)).map_err(|e| { + JsError::from(JsValue::from_str(&format!( + "Failed to convert index to JsValue: {:?}", + e + ))) + })?; + + let js_value = to_value(&value.clone()).map_err(|e| { + JsError::from(JsValue::from_str(&format!( + "Failed to convert value to JsValue: {:?}", + e + ))) + })?; + + Reflect::set(&js_array, &js_index, &js_value).map_err(|e| { + JsValue::from_str(&format!( + "Failed to set property on JsValue: {:?}", + e + )) + })?; + } + + // Attempt to cast the JsValue (Array) to the ExternType + // We use unchecked_into here for the reasons mentioned in the BTreeMap implementation + let extern_type = js_array.unchecked_into::(); + + Ok(extern_type) + } + + // Method to convert a JavaScript object representation to a VecDeque + fn from_extern(extern_type: ExternType) -> Result { + let type_name = std::any::type_name::(); + + let js_value = extern_type.dyn_into::().map_err(|_| { + JsValue::from_str(&format!( + "Failed to convert {} to JsValue", + type_name + )) + })?; + + serde_wasm_bindgen::from_value(js_value).map_err(|e| { + let error_msg = format!( + "Failed to convert {} JsValue to Vec<{}>: {:?}", + type_name, + std::any::type_name::(), + e, + ); + log_err(&error_msg); + JsValue::from_str(&error_msg) + }) + } +} + +// TODO: dedup implementation of BTreeMap/HashMap +/// Implementation of WasmType for BTreeMap with methods for converting between Rust and JavaScript types. +impl< + K: DeserializeOwned + Ord + Into + Clone + Hash, + V: Into + Clone, + ExternType: JsCast, + > WasmType for HashMap +where + V: DeserializeOwned, +{ + type RustType = HashMap; + + fn to_extern(rust_type: Self::RustType) -> Result { + // Create a new JavaScript object + let js_object = js_sys::Object::new(); + + // Set properties on the JavaScript object from the HashMap + for (key, value) in rust_type.iter() { + Reflect::set( + &js_object, + &key.clone().into(), + &value.clone().into(), + ) + .map_err(|e| { + JsValue::from_str(&format!( + "Failed to set property on JsValue: {:?}", + e + )) + })?; + } + + // Try to cast the JsValue to ExternType + // let ischemas: ExternType = js_object.dyn_into().map_err(|e| { + // JsValue::from_str(&format!( + // "Failed to cast Object `{:?}` to ExternType. Error: {:?}", + // js_object, e + // )) + // })?; + + // Unchecked here can potentially lead to problems down the line. + // However somehow `js_object.dyn_into()` messes up the casting.. + let extern_type = js_object.unchecked_into::(); + + Ok(extern_type) + } + + fn from_extern(extern_type: ExternType) -> Result { + let type_name = std::any::type_name::(); + + let js_value = extern_type.dyn_into::().map_err(|_| { + JsValue::from_str(&format!( + "Failed to convert {} to JsValue", + type_name + )) + })?; + + serde_wasm_bindgen::from_value(js_value).map_err(|e| { + let error_msg = format!( + "Failed to convert {} JsValue to HashMap: {:?}", + type_name, + std::any::type_name::(), + e, + ); + log_err(&error_msg); + JsValue::from_str(&error_msg) + }) + } +} + +impl WasmType for DateTime { + type RustType = DateTime; + + fn to_extern(rust_type: Self::RustType) -> Result { + // Convert the Rust DateTime to a string + let iso_string = rust_type.to_rfc3339(); + // Create a new JavaScript Date object from the string + let js_date = js_sys::Date::new(&JsValue::from_str(&iso_string)); + + // Check if the conversion was successful + if js_date.is_instance_of::() { + Ok(js_date) + } else { + Err(JsError::from( + "Failed to create JavaScript Date object.".to_owned(), + )) + } + } + + fn from_extern(extern_type: IDate) -> Result { + // Ensure we have a Date object + if let Some(date) = extern_type.dyn_into::().ok() { + // Convert the JavaScript Date object to an ISO string + let iso_string = + date.to_iso_string().as_string().unwrap_or_default(); + // Parse the ISO string into a Rust DateTime + DateTime::parse_from_rfc3339(&iso_string) + .map(|dt| dt.with_timezone(&Utc)) + .map_err(|e| JsError::from(e.to_string())) + } else { + Err(JsError::from( + "Input JsValue is not a Date object.".to_owned(), + )) + } + } +} From 144b33f6cbe36bd63cba1da5f25c07d2be699568 Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Sat, 11 Nov 2023 20:22:43 +0000 Subject: [PATCH 3/6] Fix OAI crate compilation --- crates/neatcoder/Cargo.toml | 2 +- crates/oai/src/models/chat/params.rs | 2 +- crates/oai/src/models/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/neatcoder/Cargo.toml b/crates/neatcoder/Cargo.toml index a9257f6..cf09809 100644 --- a/crates/neatcoder/Cargo.toml +++ b/crates/neatcoder/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["cdylib"] [dependencies] parser = { path = "../parser" } -oai = { path = "../oai", features = ["wasm"] } +oai = { path = "../oai", default-features = false, features = ["wasm"] } wasmer = { path = "../wasmer" } anyhow = "1.0" diff --git a/crates/oai/src/models/chat/params.rs b/crates/oai/src/models/chat/params.rs index 1b83ace..f7d2904 100644 --- a/crates/oai/src/models/chat/params.rs +++ b/crates/oai/src/models/chat/params.rs @@ -220,7 +220,7 @@ pub mod wasm { logit_bias: JsValue, user: Option, ) -> Result { - let mut logit_bias = jsvalue_to_hmap(logit_bias)?; + let logit_bias = jsvalue_to_hmap(logit_bias)?; let params = ChatParamsWasm( ChatParams::new( diff --git a/crates/oai/src/models/mod.rs b/crates/oai/src/models/mod.rs index 4645a5d..a962e6e 100644 --- a/crates/oai/src/models/mod.rs +++ b/crates/oai/src/models/mod.rs @@ -3,7 +3,7 @@ pub mod message; pub mod role; use ::anyhow::Result; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::wasm_bindgen; From 0a875e257149eb5954ab8966caf67282b63b5959 Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Mon, 13 Nov 2023 09:00:49 +0000 Subject: [PATCH 4/6] Fix wasm build --- .../neatcoder/src/endpoints/get_chat_title.rs | 8 ++-- .../src/endpoints/scaffold_project.rs | 8 ++-- crates/neatcoder/src/endpoints/stream_code.rs | 20 +++++----- .../src/models/app_data/interfaces/apis.rs | 8 ++-- .../src/models/app_data/interfaces/dbs.rs | 8 ++-- .../src/models/app_data/interfaces/mod.rs | 6 +-- .../src/models/app_data/interfaces/storage.rs | 8 ++-- crates/neatcoder/src/models/chat.rs | 40 +++++++++---------- crates/neatcoder/src/typescript.rs | 15 ------- crates/neatcoder/src/utils.rs | 2 +- crates/oai/src/lib.rs | 8 +++- crates/oai/src/models/chat/request.rs | 29 +++++++------- crates/oai/src/models/chat/response.rs | 4 +- crates/oai/src/models/message.rs | 28 ++++++------- 14 files changed, 92 insertions(+), 100 deletions(-) diff --git a/crates/neatcoder/src/endpoints/get_chat_title.rs b/crates/neatcoder/src/endpoints/get_chat_title.rs index cbfd1d4..8d41ca2 100644 --- a/crates/neatcoder/src/endpoints/get_chat_title.rs +++ b/crates/neatcoder/src/endpoints/get_chat_title.rs @@ -4,7 +4,7 @@ use oai::models::{ chat::{ params::wasm::ChatParamsWasm as ChatParams, request::wasm::chat_raw, }, - message::wasm::MessageWasm as AiMessage, + message::wasm::GptMessageWasm as GptMessage, role::Role as GptRole, Models as AiModels, }; @@ -15,7 +15,7 @@ pub async fn get_chat_title( ) -> Result { let mut prompts = Vec::new(); - prompts.push(AiMessage::new(GptRole::System, String::from( + prompts.push(GptMessage::new(GptRole::System, String::from( " - Context: Briefly describe the key topics or themes of the chat. - Title Specifications: The title should be concise, and not exceed 6 words. It should reflect the tone of the chat (e.g., professional, casual, informative, provocative, etc.). @@ -32,9 +32,9 @@ The title of the prompt is:", msg ); - prompts.push(AiMessage::new(GptRole::User, main_prompt)); + prompts.push(GptMessage::new(GptRole::User, main_prompt)); - let prompts = prompts.iter().map(|x| x).collect::>(); + let prompts = prompts.iter().map(|x| x).collect::>(); let ai_params = ChatParams::empty(AiModels::Gpt35Turbo).max_tokens(15); diff --git a/crates/neatcoder/src/endpoints/scaffold_project.rs b/crates/neatcoder/src/endpoints/scaffold_project.rs index bce0cfb..f63ece1 100644 --- a/crates/neatcoder/src/endpoints/scaffold_project.rs +++ b/crates/neatcoder/src/endpoints/scaffold_project.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use js_sys::{Function, JsString}; use oai::models::{ chat::params::wasm::ChatParamsWasm as ChatParams, - message::wasm::MessageWasm as AiMessage, role::Role as GptRole, + message::wasm::GptMessageWasm as GptMessage, role::Role as GptRole, }; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; @@ -47,7 +47,7 @@ pub async fn scaffold_project( ) -> Result<(Value, Files)> { let mut prompts = Vec::new(); - prompts.push(AiMessage::new(GptRole::System,format!( + prompts.push(GptMessage::new(GptRole::System,format!( "You are a software engineer who is specialised in building software in {}.", language.name() ), )); @@ -61,9 +61,9 @@ Based on the information provided write the project's folder structure, starting Answer in JSON format (Do not forget to start with ```json). For each file provide a brief description included in the json", language.name(), client_params.specs); - prompts.push(AiMessage::new(GptRole::User, main_prompt)); + prompts.push(GptMessage::new(GptRole::User, main_prompt)); - let prompts = prompts.iter().map(|x| x).collect::>(); + let prompts = prompts.iter().map(|x| x).collect::>(); let (_, mut scaffold_json) = write_json(&ai_params, &prompts, request_callback).await?; diff --git a/crates/neatcoder/src/endpoints/stream_code.rs b/crates/neatcoder/src/endpoints/stream_code.rs index 2b3240c..b3e72b6 100644 --- a/crates/neatcoder/src/endpoints/stream_code.rs +++ b/crates/neatcoder/src/endpoints/stream_code.rs @@ -4,7 +4,9 @@ use oai::models::{ chat::{ params::wasm::ChatParamsWasm as ChatParams, request::request_stream, }, - message::{wasm::MessageWasm as AiMessage, Message as AiMessageInner}, + message::{ + wasm::GptMessageWasm as GptMessage, GptMessage as GptMessageInner, + }, role::Role as GptRole, }; use serde::{Deserialize, Serialize}; @@ -72,7 +74,7 @@ pub fn stream_code( anyhow!("It seems that the the field `specs` is missing..") })?; - prompts.push(AiMessage::new( + prompts.push(GptMessage::new( GptRole::System, format!( "You are a software engineer who is specialised in {}.", @@ -80,7 +82,7 @@ pub fn stream_code( ), )); - prompts.push(AiMessage::new( + prompts.push(GptMessage::new( GptRole::User, String::from(project_description), )); @@ -90,10 +92,10 @@ pub fn stream_code( .get(file) .ok_or_else(|| anyhow!("Unable to find fild {:?}", file))?; - prompts.push(AiMessage::new(GptRole::User, code.clone())); + prompts.push(GptMessage::new(GptRole::User, code.clone())); } - prompts.push(AiMessage::new(GptRole::User, project_scaffold.to_string())); + prompts.push(GptMessage::new(GptRole::User, project_scaffold.to_string())); let mut main_prompt = format!( " @@ -125,13 +127,13 @@ pub fn stream_code( )); } - prompts.push(AiMessage::new(GptRole::User, main_prompt)); + prompts.push(GptMessage::new(GptRole::User, main_prompt)); - // Assuming prompts is a Vec<&AiMessageWasm> - let msgs: Vec<&AiMessageInner> = + // Assuming prompts is a Vec<&GptMessageWasm> + let msgs: Vec<&GptMessageInner> = prompts.iter().map(|m_wasm| (*m_wasm).deref()).collect(); - let prompts_slice: &[&AiMessageInner] = &msgs; + let prompts_slice: &[&GptMessageInner] = &msgs; let request_body = request_stream(ai_params.deref(), &prompts_slice, &[], &[])?; diff --git a/crates/neatcoder/src/models/app_data/interfaces/apis.rs b/crates/neatcoder/src/models/app_data/interfaces/apis.rs index 0320746..e64343d 100644 --- a/crates/neatcoder/src/models/app_data/interfaces/apis.rs +++ b/crates/neatcoder/src/models/app_data/interfaces/apis.rs @@ -3,7 +3,7 @@ use crate::typescript::ISchemas; use anyhow::Result; use js_sys::JsString; use oai::models::{ - message::wasm::MessageWasm as AiMessage, role::Role as GptRole, + message::wasm::GptMessageWasm as GptMessage, role::Role as GptRole, }; use serde::{Deserialize, Serialize}; use std::{ @@ -154,7 +154,7 @@ impl Api { } impl AsContext for Api { - fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { + fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { let mut main_prompt = format!( " Have in consideration the following {} communication service: @@ -172,14 +172,14 @@ Have in consideration the following {} communication service: main_prompt = format!("{}\n{} {}", main_prompt, "- host:", host); } - msg_sequence.push(AiMessage::new(GptRole::User, main_prompt)); + msg_sequence.push(GptMessage::new(GptRole::User, main_prompt)); for (schema_name, schema) in self.schemas.iter() { let prompt = format!(" Consider the following schema as part of the {} database. It's called `{}` and the schema is:\n```\n{}``` ", self.name, schema_name, schema); - msg_sequence.push(AiMessage::new(GptRole::User, prompt)); + msg_sequence.push(GptMessage::new(GptRole::User, prompt)); } Ok(()) diff --git a/crates/neatcoder/src/models/app_data/interfaces/dbs.rs b/crates/neatcoder/src/models/app_data/interfaces/dbs.rs index 28813d2..6efad64 100644 --- a/crates/neatcoder/src/models/app_data/interfaces/dbs.rs +++ b/crates/neatcoder/src/models/app_data/interfaces/dbs.rs @@ -3,7 +3,7 @@ use crate::typescript::ISchemas; use anyhow::Result; use js_sys::JsString; use oai::models::{ - message::wasm::MessageWasm as AiMessage, role::Role as GptRole, + message::wasm::GptMessageWasm as GptMessage, role::Role as GptRole, }; use serde::{Deserialize, Serialize}; use std::{ @@ -232,7 +232,7 @@ pub enum DbType { } impl AsContext for Database { - fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { + fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { let mut main_prompt = format!( " Have in consideration the following {} Database: @@ -252,14 +252,14 @@ Have in consideration the following {} Database: format!("{}\n{} {}", main_prompt, "- database host:", host); } - msg_sequence.push(AiMessage::new(GptRole::User, main_prompt)); + msg_sequence.push(GptMessage::new(GptRole::User, main_prompt)); for (schema_name, schema) in self.schemas.iter() { let prompt = format!(" Consider the following schema as part of the {} database. It's called `{}` and the schema is:\n```\n{}``` ", self.name, schema_name, schema); - msg_sequence.push(AiMessage::new(GptRole::User, prompt)); + msg_sequence.push(GptMessage::new(GptRole::User, prompt)); } Ok(()) diff --git a/crates/neatcoder/src/models/app_data/interfaces/mod.rs b/crates/neatcoder/src/models/app_data/interfaces/mod.rs index fcfe29e..f99d2cb 100644 --- a/crates/neatcoder/src/models/app_data/interfaces/mod.rs +++ b/crates/neatcoder/src/models/app_data/interfaces/mod.rs @@ -5,7 +5,7 @@ pub mod storage; use self::{apis::Api, dbs::Database, storage::Storage}; use crate::typescript::ISchemas; use anyhow::{anyhow, Result}; -use oai::models::message::wasm::MessageWasm as AiMessage; +use oai::models::message::wasm::GptMessageWasm as GptMessage; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; @@ -171,11 +171,11 @@ pub type SchemaFile = String; /// Trait that injects the context of interfaces onto the LLM Message Sequence. pub trait AsContext { - fn add_context(&self, msg_sequence: &mut Vec) -> Result<()>; + fn add_context(&self, msg_sequence: &mut Vec) -> Result<()>; } impl AsContext for Interface { - fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { + fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { match self.interface_type { InterfaceType::Database => self .inner diff --git a/crates/neatcoder/src/models/app_data/interfaces/storage.rs b/crates/neatcoder/src/models/app_data/interfaces/storage.rs index e32b4fb..a788c40 100644 --- a/crates/neatcoder/src/models/app_data/interfaces/storage.rs +++ b/crates/neatcoder/src/models/app_data/interfaces/storage.rs @@ -2,7 +2,7 @@ use crate::typescript::ISchemas; use anyhow::Result; use js_sys::JsString; use oai::models::{ - message::wasm::MessageWasm as AiMessage, role::Role as GptRole, + message::wasm::GptMessageWasm as GptMessage, role::Role as GptRole, }; use serde::{Deserialize, Serialize}; use std::{ @@ -141,7 +141,7 @@ impl Storage { } impl AsContext for Storage { - fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { + fn add_context(&self, msg_sequence: &mut Vec) -> Result<()> { let mut main_prompt = format!( " Have in consideration the following {} data storage: @@ -157,14 +157,14 @@ Have in consideration the following {} data storage: format!("{}\n{} {}", main_prompt, "- region:", region); } - msg_sequence.push(AiMessage::new(GptRole::User, main_prompt)); + msg_sequence.push(GptMessage::new(GptRole::User, main_prompt)); for (schema_name, schema) in self.schemas.iter() { let prompt = format!(" Consider the following {} schema as part of the {} data storage. It's called `{}` and the schema is:\n```\n{}``` ", self.file_type, self.name, schema_name, schema); - msg_sequence.push(AiMessage::new(GptRole::User, prompt)); + msg_sequence.push(GptMessage::new(GptRole::User, prompt)); } Ok(()) diff --git a/crates/neatcoder/src/models/chat.rs b/crates/neatcoder/src/models/chat.rs index 1516847..3a6b4a2 100644 --- a/crates/neatcoder/src/models/chat.rs +++ b/crates/neatcoder/src/models/chat.rs @@ -1,17 +1,15 @@ use anyhow::Result; use chrono::{DateTime, Utc}; use js_sys::{Date as IDate, Function, JsString}; -use oai::models::{ - message::wasm::MessageWasm as AiMessage, Models as AiModels, +use oai::{ + foreign::{IMessages, IModels}, + models::{message::wasm::GptMessageWasm as GptMessage, Models as AiModels}, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; -use crate::{ - endpoints::get_chat_title::get_chat_title, - typescript::{IMessages, IModels}, -}; +use crate::endpoints::get_chat_title::get_chat_title; use wasmer::{JsError, WasmType}; @@ -21,8 +19,8 @@ use wasmer::{JsError, WasmType}; pub struct Chat { pub(crate) session_id: String, pub(crate) title: String, - pub(crate) models: HashMap, - pub(crate) messages: Vec, + pub(crate) models: HashMap, + pub(crate) messages: Vec, } #[wasm_bindgen] @@ -81,7 +79,7 @@ impl Chat { } #[wasm_bindgen(js_name = addMessage)] - pub fn add_message(&mut self, message: Message) { + pub fn add_message(&mut self, message: MessageData) { self.messages.push(message); } @@ -99,7 +97,7 @@ impl Chat { let model_id = model.as_str(); self.models - .insert(model_id.to_string(), Model::new(model_id.to_string())); + .insert(model_id.to_string(), ModelData::new(model_id.to_string())); Ok(()) } @@ -123,17 +121,17 @@ impl Chat { #[wasm_bindgen] #[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Model { +#[serde(rename_all = "camelCase", rename = "Model")] +pub struct ModelData { pub(crate) id: String, pub(crate) uri: String, pub(crate) interface: String, } #[wasm_bindgen] -impl Model { +impl ModelData { #[wasm_bindgen(constructor)] - pub fn new(id: String) -> Model { + pub fn new(id: String) -> ModelData { Self { id, uri: String::from("https://api.openai.com/v1/chat/completions"), @@ -159,21 +157,21 @@ impl Model { #[wasm_bindgen] #[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Message { +#[serde(rename_all = "camelCase", rename = "Message")] +pub struct MessageData { pub(crate) user: String, pub(crate) ts: DateTime, - pub(crate) payload: AiMessage, + pub(crate) payload: GptMessage, } #[wasm_bindgen] -impl Message { +impl MessageData { #[wasm_bindgen(constructor)] pub fn new( user: String, ts: IDate, - payload: AiMessage, - ) -> Result { + payload: GptMessage, + ) -> Result { let datetime = DateTime::from_extern(ts.into())?; Ok(Self { user, @@ -193,7 +191,7 @@ impl Message { } #[wasm_bindgen(getter)] - pub fn payload(&self) -> AiMessage { + pub fn payload(&self) -> GptMessage { self.payload.clone() } } diff --git a/crates/neatcoder/src/typescript.rs b/crates/neatcoder/src/typescript.rs index 615458e..87dbc01 100644 --- a/crates/neatcoder/src/typescript.rs +++ b/crates/neatcoder/src/typescript.rs @@ -1,15 +1,6 @@ ///< Provides foreign typescript types use wasm_bindgen::prelude::wasm_bindgen; -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(typescript_type = "Record")] - pub type IModels; - - #[wasm_bindgen(typescript_type = "Array")] - pub type IMessages; -} - #[wasm_bindgen] extern "C" { #[wasm_bindgen(typescript_type = "Record")] @@ -36,9 +27,3 @@ extern "C" { #[wasm_bindgen(typescript_type = "Array")] pub type IOrder; } - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(typescript_type = "Array")] - pub type IOpenAIMsg; -} diff --git a/crates/neatcoder/src/utils.rs b/crates/neatcoder/src/utils.rs index 1f7f4cb..906f2e6 100644 --- a/crates/neatcoder/src/utils.rs +++ b/crates/neatcoder/src/utils.rs @@ -4,7 +4,7 @@ use js_sys::Function; use oai::models::chat::{ params::wasm::ChatParamsWasm as ChatParams, request::wasm::chat_raw, }; -use oai::models::message::wasm::MessageWasm as GptMessage; +use oai::models::message::wasm::GptMessageWasm as GptMessage; use parser::parser::json::AsJson; use serde_json::Value; use wasmer::log; diff --git a/crates/oai/src/lib.rs b/crates/oai/src/lib.rs index 1762113..7e33959 100644 --- a/crates/oai/src/lib.rs +++ b/crates/oai/src/lib.rs @@ -11,7 +11,13 @@ pub mod foreign { #[wasm_bindgen(typescript_type = "Record")] pub type IModels; - #[wasm_bindgen(typescript_type = "Array")] + #[wasm_bindgen(typescript_type = "Array")] pub type IMessages; } + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(typescript_type = "Array")] + pub type IGptMessage; + } } diff --git a/crates/oai/src/models/chat/request.rs b/crates/oai/src/models/chat/request.rs index c26c61d..5b3224f 100644 --- a/crates/oai/src/models/chat/request.rs +++ b/crates/oai/src/models/chat/request.rs @@ -4,7 +4,7 @@ use std::ops::Deref; use crate::models::{ chat::{params::ChatParams, response::ChatResponse}, - message::Message, + message::GptMessage, }; #[cfg(not(feature = "wasm"))] @@ -110,7 +110,7 @@ pub mod wasm { use crate::{ foreign::IMessages, models::{ - chat::params::wasm::ChatParamsWasm, message::wasm::MessageWasm, + chat::params::wasm::ChatParamsWasm, message::wasm::GptMessageWasm, }, }; @@ -124,7 +124,7 @@ pub mod wasm { pub async fn chat( request_callback: &Function, ai_params: &ChatParamsWasm, - msgs: &[&MessageWasm], + msgs: &[&GptMessageWasm], funcs: &[&String], stop_seq: &[String], ) -> Result { @@ -148,14 +148,14 @@ pub mod wasm { pub async fn chat_raw( request_callback: &Function, ai_params: &ChatParamsWasm, - msgs: &[&MessageWasm], + msgs: &[&GptMessageWasm], funcs: &[&String], stop_seq: &[String], ) -> Result { - let msgs: Vec<&Message> = + let msgs: Vec<&GptMessage> = msgs.iter().map(|&m_wasm| m_wasm.deref()).collect(); - let msg_slice: &[&Message] = &msgs; + let msg_slice: &[&GptMessage] = &msgs; let req_body = request_body(ai_params.deref(), msg_slice, funcs, stop_seq, false)?; @@ -195,12 +195,13 @@ pub mod wasm { job: ChatParamsWasm, stream: bool, ) -> Result { - let msgs: Vec = Vec::from_extern(msgs).map_err(|e| { - JsValue::from_str(&format!( - "Failed to convert msgs to native Wasm type: {:?}", - e - )) - })?; + let msgs: Vec = + Vec::from_extern(msgs).map_err(|e| { + JsValue::from_str(&format!( + "Failed to convert msgs to native Wasm type: {:?}", + e + )) + })?; let mut data = json!({ "model": job.model.as_str(), @@ -224,7 +225,7 @@ pub mod wasm { pub fn request_stream( ai_params: impl Deref, - msgs: &[&Message], + msgs: &[&GptMessage], funcs: &[&String], stop_seq: &[String], ) -> Result { @@ -237,7 +238,7 @@ pub fn request_stream( pub fn request_body( job: impl Deref, - msgs: &[&Message], + msgs: &[&GptMessage], funcs: &[&String], // TODO: Add to ChatParams stop_seq: &[String], // TODO: Add to ChatParams stream: bool, diff --git a/crates/oai/src/models/chat/response.rs b/crates/oai/src/models/chat/response.rs index b058547..44b8ee0 100644 --- a/crates/oai/src/models/chat/response.rs +++ b/crates/oai/src/models/chat/response.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::models::message::Message; +use crate::models::message::GptMessage; #[derive(Serialize, Deserialize, Debug)] pub struct ApiResponse { @@ -47,7 +47,7 @@ pub struct ChatResponse { #[derive(Serialize, Deserialize, Debug)] pub struct Choice { pub index: i32, - pub message: Message, + pub message: GptMessage, pub finish_reason: String, } diff --git a/crates/oai/src/models/message.rs b/crates/oai/src/models/message.rs index d4fbdb9..2ff0c19 100644 --- a/crates/oai/src/models/message.rs +++ b/crates/oai/src/models/message.rs @@ -3,12 +3,12 @@ use serde::{Deserialize, Serialize}; use super::role::Role; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Message { +pub struct GptMessage { pub role: Role, pub content: String, } -impl Message { +impl GptMessage { pub fn user(content: &str) -> Self { Self { role: Role::User, @@ -43,17 +43,17 @@ pub mod wasm { use crate::models::role::Role; - use super::Message; + use super::GptMessage; #[wasm_bindgen(js_name = "Message")] #[derive(Debug, Clone)] - pub struct MessageWasm(Message); + pub struct GptMessageWasm(GptMessage); #[wasm_bindgen] - impl MessageWasm { + impl GptMessageWasm { #[wasm_bindgen(constructor)] pub fn new(role: Role, content: String) -> Self { - Self(Message { role, content }) + Self(GptMessage { role, content }) } #[wasm_bindgen(getter)] @@ -67,28 +67,28 @@ pub mod wasm { } } - impl AsRef for MessageWasm { - fn as_ref(&self) -> &Message { + impl AsRef for GptMessageWasm { + fn as_ref(&self) -> &GptMessage { &self.0 } } - impl Deref for MessageWasm { - type Target = Message; + impl Deref for GptMessageWasm { + type Target = GptMessage; fn deref(&self) -> &Self::Target { &self.0 } } - impl DerefMut for MessageWasm { + impl DerefMut for GptMessageWasm { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } // Implement Serialize for MessageWasm by delegating to Message. - impl Serialize for MessageWasm { + impl Serialize for GptMessageWasm { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -99,13 +99,13 @@ pub mod wasm { } // Implement Deserialize for MessageWasm by delegating to Message. - impl<'de> Deserialize<'de> for MessageWasm { + impl<'de> Deserialize<'de> for GptMessageWasm { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { // Deserialize as Message and wrap inside MessageWasm. - Message::deserialize(deserializer).map(MessageWasm) + GptMessage::deserialize(deserializer).map(GptMessageWasm) } } } From 48891a89a3b99252616711ef5aa163bc02a0508a Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Wed, 15 Nov 2023 21:39:33 +0000 Subject: [PATCH 5/6] Fix neatcoder build --- .../neatcoder/src/endpoints/get_chat_title.rs | 4 +- crates/neatcoder/src/models/chat.rs | 209 ++------------ crates/oai/Cargo.toml | 2 + crates/oai/src/consts.rs | 2 + crates/oai/src/http.rs | 112 ++++++++ crates/oai/src/lib.rs | 9 +- crates/oai/src/models/assistant/assistant.rs | 172 ++++++++++++ crates/oai/src/models/assistant/file.rs | 188 +++++++++++++ crates/oai/src/models/assistant/message.rs | 108 ++++++++ crates/oai/src/models/assistant/mod.rs | 220 +++++++++++++++ crates/oai/src/models/assistant/run.rs | 255 ++++++++++++++++++ crates/oai/src/models/assistant/thread.rs | 69 +++++ crates/oai/src/models/chat/client.rs | 0 crates/oai/src/models/chat/mod.rs | 209 ++++++++++++++ crates/oai/src/models/chat/params.rs | 14 +- crates/oai/src/models/chat/request.rs | 22 +- crates/oai/src/models/message.rs | 2 +- crates/oai/src/models/mod.rs | 41 +-- crates/oai/src/models/model.rs | 93 +++++++ 19 files changed, 1476 insertions(+), 255 deletions(-) create mode 100644 crates/oai/src/consts.rs create mode 100644 crates/oai/src/http.rs create mode 100644 crates/oai/src/models/assistant/assistant.rs create mode 100644 crates/oai/src/models/assistant/file.rs create mode 100644 crates/oai/src/models/assistant/message.rs create mode 100644 crates/oai/src/models/assistant/mod.rs create mode 100644 crates/oai/src/models/assistant/run.rs create mode 100644 crates/oai/src/models/assistant/thread.rs delete mode 100644 crates/oai/src/models/chat/client.rs create mode 100644 crates/oai/src/models/model.rs diff --git a/crates/neatcoder/src/endpoints/get_chat_title.rs b/crates/neatcoder/src/endpoints/get_chat_title.rs index 8d41ca2..330977b 100644 --- a/crates/neatcoder/src/endpoints/get_chat_title.rs +++ b/crates/neatcoder/src/endpoints/get_chat_title.rs @@ -5,8 +5,8 @@ use oai::models::{ params::wasm::ChatParamsWasm as ChatParams, request::wasm::chat_raw, }, message::wasm::GptMessageWasm as GptMessage, + model::GptModel, role::Role as GptRole, - Models as AiModels, }; pub async fn get_chat_title( @@ -36,7 +36,7 @@ The title of the prompt is:", let prompts = prompts.iter().map(|x| x).collect::>(); - let ai_params = ChatParams::empty(AiModels::Gpt35Turbo).max_tokens(15); + let ai_params = ChatParams::empty(GptModel::Gpt35Turbo).max_tokens(15); let chat = chat_raw(request_callback, &ai_params, &prompts, &[], &[]).await?; diff --git a/crates/neatcoder/src/models/chat.rs b/crates/neatcoder/src/models/chat.rs index 3a6b4a2..ef3809f 100644 --- a/crates/neatcoder/src/models/chat.rs +++ b/crates/neatcoder/src/models/chat.rs @@ -1,197 +1,30 @@ use anyhow::Result; -use chrono::{DateTime, Utc}; -use js_sys::{Date as IDate, Function, JsString}; -use oai::{ - foreign::{IMessages, IModels}, - models::{message::wasm::GptMessageWasm as GptMessage, Models as AiModels}, -}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use js_sys::Function; +use oai::models::chat::wasm::ChatWasm; use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; +use wasmer::JsError; use crate::endpoints::get_chat_title::get_chat_title; -use wasmer::{JsError, WasmType}; - -#[wasm_bindgen] -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Chat { - pub(crate) session_id: String, - pub(crate) title: String, - pub(crate) models: HashMap, - pub(crate) messages: Vec, -} - -#[wasm_bindgen] -impl Chat { - #[wasm_bindgen(constructor)] - pub fn new(session_id: String, title: String) -> Chat { - Self { - session_id, - title, - models: HashMap::new(), - messages: Vec::new(), - } - } - - #[wasm_bindgen(getter, js_name = sessionId)] - pub fn session_id(&self) -> JsString { - self.session_id.clone().into() - } - - #[wasm_bindgen(getter)] - pub fn title(&self) -> JsString { - self.title.clone().into() - } - - #[wasm_bindgen(getter)] - pub fn models(&self) -> Result { - HashMap::to_extern(self.models.clone()) - } - - #[wasm_bindgen(getter)] - pub fn messages(&self) -> Result { - Vec::to_extern(self.messages.clone()) - } - - #[wasm_bindgen(js_name = setTitle)] - pub async fn set_title( - &mut self, - request_callback: &Function, - ) -> Result<(), JsError> { - if !self.messages.is_empty() { - // Get the first element using indexing (index 0) - let first_msg = self.messages[0].clone(); - let title = - get_chat_title(&first_msg.payload.content, request_callback) - .await - .map_err(|e| JsError::from_str(&e.to_string()))?; - - self.title = title; - - Ok(()) - } else { - Err(JsError::from(JsValue::from_str( - "Unable to create title. No messages in the Chat.", - ))) - } - } - - #[wasm_bindgen(js_name = addMessage)] - pub fn add_message(&mut self, message: MessageData) { - self.messages.push(message); - } - - #[wasm_bindgen(js_name = setMessages)] - pub fn set_messages(&mut self, messages: IMessages) -> Result<(), JsError> { - let messages = Vec::from_extern(messages)?; - - self.messages = messages; +#[wasm_bindgen(js_name = setTitle)] +pub async fn set_title( + chat: &mut ChatWasm, + request_callback: &Function, +) -> Result<(), JsError> { + if !chat.messages.is_empty() { + // Get the first element using indexing (index 0) + let first_msg = chat.messages[0].clone(); + let title = + get_chat_title(&first_msg.payload.content, request_callback) + .await + .map_err(|e| JsError::from_str(&e.to_string()))?; + + chat.title = title; Ok(()) - } - - #[wasm_bindgen(js_name = addModel)] - pub fn add_model(&mut self, model: AiModels) -> Result<(), JsError> { - let model_id = model.as_str(); - - self.models - .insert(model_id.to_string(), ModelData::new(model_id.to_string())); - - Ok(()) - } - - #[wasm_bindgen(js_name = castFromString)] - pub fn cast_from_string(json: String) -> Result { - let chat = serde_json::from_str(&json) - .map_err(|e| JsError::from_str(&e.to_string()))?; - - Ok(chat) - } - - #[wasm_bindgen(js_name = castToString)] - pub fn cast_to_string(&self) -> Result { - let json = serde_json::to_string_pretty(self) - .map_err(|e| JsError::from_str(&e.to_string()))?; - - Ok(json.into()) - } -} - -#[wasm_bindgen] -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase", rename = "Model")] -pub struct ModelData { - pub(crate) id: String, - pub(crate) uri: String, - pub(crate) interface: String, -} - -#[wasm_bindgen] -impl ModelData { - #[wasm_bindgen(constructor)] - pub fn new(id: String) -> ModelData { - Self { - id, - uri: String::from("https://api.openai.com/v1/chat/completions"), - interface: String::from("OpenAI"), - } - } - - #[wasm_bindgen(getter)] - pub fn id(&self) -> JsString { - self.id.clone().into() - } - - #[wasm_bindgen(getter)] - pub fn uri(&self) -> JsString { - self.uri.clone().into() - } - - #[wasm_bindgen(getter)] - pub fn interface(&self) -> JsString { - self.interface.clone().into() - } -} - -#[wasm_bindgen] -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase", rename = "Message")] -pub struct MessageData { - pub(crate) user: String, - pub(crate) ts: DateTime, - pub(crate) payload: GptMessage, -} - -#[wasm_bindgen] -impl MessageData { - #[wasm_bindgen(constructor)] - pub fn new( - user: String, - ts: IDate, - payload: GptMessage, - ) -> Result { - let datetime = DateTime::from_extern(ts.into())?; - Ok(Self { - user, - ts: datetime, - payload, - }) - } - - #[wasm_bindgen(getter)] - pub fn user(&self) -> JsString { - self.user.clone().into() - } - - #[wasm_bindgen(getter)] - pub fn ts(&self) -> Result { - DateTime::to_extern(self.ts.clone().into()) - } - - #[wasm_bindgen(getter)] - pub fn payload(&self) -> GptMessage { - self.payload.clone() + } else { + Err(JsError::from(JsValue::from_str( + "Unable to create title. No messages in the Chat.", + ))) } } diff --git a/crates/oai/Cargo.toml b/crates/oai/Cargo.toml index 24f7f43..65ae5d1 100644 --- a/crates/oai/Cargo.toml +++ b/crates/oai/Cargo.toml @@ -29,6 +29,8 @@ futures = { version = "0.3.29", optional = true } bytes = { version = "1.5.0", optional = true } tokio = { version = "1.34.0", optional = true } wasmer = { path = "../wasmer", optional = true } +chrono = {version = "0.4.31", features = ["serde"]} +derive_more = "0.99.17" [dev-dependencies] diff --git a/crates/oai/src/consts.rs b/crates/oai/src/consts.rs new file mode 100644 index 0000000..6c35084 --- /dev/null +++ b/crates/oai/src/consts.rs @@ -0,0 +1,2 @@ +// Constants +pub const BASE_BETA_URL: &str = "https://api.openai.com/v1"; diff --git a/crates/oai/src/http.rs b/crates/oai/src/http.rs new file mode 100644 index 0000000..a37700c --- /dev/null +++ b/crates/oai/src/http.rs @@ -0,0 +1,112 @@ +use crate::print_; +use anyhow::{anyhow, Result}; +use reqwest::{header::HeaderMap, Client}; +use serde_json::Value; + +use crate::consts::BASE_BETA_URL; + +pub async fn post_api( + client: &Client, + headers: &HeaderMap, + route: &str, + payload: &Value, +) -> Result { + print_!("CALLING: {}/{}", BASE_BETA_URL, route); + print_!("PAYLOAD: {}", payload); + + let response = client + .post(&format!("{}/{}", BASE_BETA_URL, route)) + .headers(headers.clone()) + .json(payload) + .send() + .await?; + + if response.status().is_success() { + let json_value = response.json::().await?; + print_!("JSON Body: {:?}", json_value); + + Ok(json_value) + } else { + print_!("API Error on route {}", route); + // If not successful, perhaps you want to parse it differently or handle the error + Err(anyhow!(response.status())) + } +} + +pub async fn delete_api( + client: &Client, + headers: &HeaderMap, + route: &str, + payload: &Value, +) -> Result { + print_!("CALLING: {}/{}", BASE_BETA_URL, route); + print_!("PAYLOAD: {}", payload); + + let response = client + .delete(&format!("{}/{}", BASE_BETA_URL, route)) + .headers(headers.clone()) + .json(payload) + .send() + .await?; + + if response.status().is_success() { + let json_value = response.json::().await?; + print_!("JSON Body: {:?}", json_value); + + Ok(json_value) + } else { + print_!("API Error on route {}", route); + // If not successful, perhaps you want to parse it differently or handle the error + Err(anyhow!(response.status())) + } +} + +pub async fn get_api( + client: &Client, + headers: &HeaderMap, + route: &str, + payload: Option<&Value>, +) -> Result { + let req = client + .get(&format!("{}/{}", BASE_BETA_URL, route)) + .headers(headers.clone()); + + let response = if let Some(payload) = payload { + req.json(payload).send().await? + } else { + req.send().await? + }; + + if response.status().is_success() { + let json_value = response.json::().await?; + print_!("JSON Body: {:?}", json_value); + + Ok(json_value) + } else { + print_!("API Err on route {}", route); + // If not successful, perhaps you want to parse it differently or handle the error + Err(anyhow!(response.status())) + } +} + +#[macro_export] +macro_rules! print_ { + ($($arg:tt)*) => { + { + use std::io::Write; + + // Print to the console + println!($($arg)*); + + let log_file_name = &$crate::LOG_FILE_NAME; + + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(log_file_name.as_str()) + .unwrap(); + + writeln!(file, $($arg)*).unwrap(); + } + }; +} diff --git a/crates/oai/src/lib.rs b/crates/oai/src/lib.rs index 7e33959..1e7bfbd 100644 --- a/crates/oai/src/lib.rs +++ b/crates/oai/src/lib.rs @@ -1,3 +1,4 @@ +pub mod consts; ///< Client for interacting with the OpenAI API. pub mod models; pub mod utils; @@ -8,11 +9,11 @@ pub mod foreign { #[wasm_bindgen] extern "C" { - #[wasm_bindgen(typescript_type = "Record")] - pub type IModels; + #[wasm_bindgen(typescript_type = "Record")] + pub type IModelsData; - #[wasm_bindgen(typescript_type = "Array")] - pub type IMessages; + #[wasm_bindgen(typescript_type = "Array")] + pub type IMessagesData; } #[wasm_bindgen] diff --git a/crates/oai/src/models/assistant/assistant.rs b/crates/oai/src/models/assistant/assistant.rs new file mode 100644 index 0000000..858b4f6 --- /dev/null +++ b/crates/oai/src/models/assistant/assistant.rs @@ -0,0 +1,172 @@ +use anyhow::Result; +use reqwest::{header::HeaderMap, Client}; +use serde::{ + de::{self, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_json::json; +use std::collections::HashMap; + +use super::OpenAIModels; +use crate::print_; +use crate::utils::post_api; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Assistant { + pub id: String, + pub object: String, + pub created_at: u32, // TODO: Should be a timestamp + pub name: String, + pub description: Option, + pub model: OpenAIModels, + pub instructions: Option, + pub tools: Vec, + pub file_ids: Vec, + pub metadata: HashMap, +} + +#[derive(Debug)] +pub enum Tool { + CodeInterpreter, + Retrieval, + FunctionCall, +} + +impl Serialize for Tool { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::SerializeMap; + + let tool_str = self.as_string(); + let mut map_ser = serializer.serialize_map(Some(1))?; + map_ser.serialize_entry("type", &tool_str)?; + map_ser.end() + } +} + +impl Tool { + pub fn new(tool: String) -> Self { + let tool = match tool.as_str() { + "code_interpreter" => Tool::CodeInterpreter, + "retrieval" => Tool::Retrieval, + "function" => Tool::FunctionCall, + _ => panic!("Invalid tool {}", tool), + }; + + tool + } + + pub fn as_string(&self) -> String { + match self { + Tool::CodeInterpreter => String::from("code_interpreter"), + Tool::Retrieval => String::from("retrieval"), + Tool::FunctionCall => String::from("function"), + } + } +} + +impl<'de> Deserialize<'de> for Tool { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ToolVisitor; + + impl<'de> Visitor<'de> for ToolVisitor { + type Value = Tool; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str(r#"a map with a key "type" representing an OpenAI tool"#) + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut tool_type = None; + while let Some(key) = map.next_key::()? { + if key == "type" { + tool_type = Some(map.next_value::()?); + break; + } + } + match tool_type { + Some(t) => match t.as_ref() { + "code_interpreter" => Ok(Tool::CodeInterpreter), + "retrieval" => Ok(Tool::Retrieval), + "function_call" => Ok(Tool::FunctionCall), + _ => Err(de::Error::unknown_field(&t, FIELDS)), + }, + None => Err(de::Error::missing_field("type")), + } + } + } + + const FIELDS: &'static [&'static str] = &["type"]; + deserializer.deserialize_map(ToolVisitor) + } +} + +impl Assistant { + pub async fn create_assistant( + client: &Client, + headers: &HeaderMap, + name: String, + instructions: String, + tools: Vec, + model: OpenAIModels, + ) -> Result { + let payload = json!({ + "name": name, // "Math Tutor", + "instructions": instructions, // "You are a personal math tutor. Write and run code to answer math questions.", + "tools": tools, // [{"type": "code_interpreter"}], + "model": model.as_string(), // "gpt-4-1106-preview" + }); + + let response_body = post_api(client, headers, "assistants", &payload).await?; + let assistant: Assistant = serde_json::from_value(response_body)?; + + print_!("Assistant: {:?}", assistant); + + Ok(assistant) + } + + pub async fn add_file() {} +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialization() -> Result<()> { + let json_data = json!({ + "created_at": 1699447210, + "description": null, + "file_ids": [], + "id": "asst_I1qwkQwPuWRQYlQtXQrny8il", + "instructions": "You are a personal math tutor. Write and run code to answer math questions.", + "metadata": {}, + "model": "gpt-4-1106-preview", + "name": "Math Tutor", + "object": "assistant", + "tools": [{"type": "code_interpreter"}] + }).to_string(); + + let assistant: Assistant = serde_json::from_str(&json_data)?; + + assert_eq!(assistant.id, "asst_I1qwkQwPuWRQYlQtXQrny8il"); + assert_eq!(assistant.name, "Math Tutor"); + assert!(matches!(assistant.model, OpenAIModels::Gpt41106Preview)); + assert!(!assistant.file_ids.iter().any(|id| id == "unexpected_id")); + + assert!(matches!( + assistant.tools.as_slice(), + [Tool::CodeInterpreter] + )); + + Ok(()) + } +} diff --git a/crates/oai/src/models/assistant/file.rs b/crates/oai/src/models/assistant/file.rs new file mode 100644 index 0000000..cf49b25 --- /dev/null +++ b/crates/oai/src/models/assistant/file.rs @@ -0,0 +1,188 @@ +use anyhow::{anyhow, Result}; +use futures_util::TryStreamExt; +use reqwest::{header::HeaderMap, Body, Client}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::path::Path; + +use tokio::fs::File as TokioFile; +use tokio_util::codec::{BytesCodec, FramedRead}; + +use super::FileID; +use crate::print_; +use crate::{ + consts::BASE_BETA_URL, + models::get_data, + utils::{delete_api, get_api}, +}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct AgentFile { + pub id: FileID, + pub object: String, // "file", + pub bytes: u32, // 120000, + pub created_at: u32, // Timestamp.. 1677610602, + pub filename: String, // "salesOverview.pdf", + pub purpose: FilePurpose, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum FilePurpose { + #[serde(rename = "fine-tune")] + FineTune, + #[serde(rename = "fine-tune-results")] + FineTuneResults, + #[serde(rename = "assistants")] + Assistants, + #[serde(rename = "assistants-output")] + AssistantsOutput, +} + +impl FilePurpose { + pub fn from_str(file_purpose: &str) -> Result { + serde_json::from_str(file_purpose).map_err(Into::into) + } + + pub fn as_str(&self) -> Result { + serde_plain::to_string(self).map_err(Into::into) + } +} + +impl AgentFile { + pub async fn list_files(client: &Client, headers: &HeaderMap) -> Result> { + let response_body = get_api(client, headers, "files", None).await?; + + let files_json = get_data(&response_body)?; + let files: Vec = serde_json::from_value(files_json.clone())?; + + print_!("Files: {:?}", files); + + Ok(files) + } + + pub async fn upload_file( + client: &Client, + headers: &HeaderMap, + file_path: &Path, + purpose: FilePurpose, + ) -> Result { + let file_json = upload_file(client, headers, file_path, &purpose.as_str()?).await?; + + serde_json::from_value(file_json.clone()).map_err(Into::into) + } + + pub async fn delete_file(client: &Client, headers: &HeaderMap, file_id: &str) -> Result { + let payload = json!({ + "file_id": file_id, + }); + + let response_body = + delete_api(client, headers, &format!("files/{}", file_id), &payload).await?; + + Ok(response_body) + } + + pub async fn retrieve_file( + client: &Client, + headers: &HeaderMap, + file_id: &str, + ) -> Result { + let payload = json!({ + "file_id": file_id, + }); + + let response_body = get_api( + client, + headers, + &format!("files/{}", file_id), + Some(&payload), + ) + .await?; + + let file_data = get_data(&response_body)?; + + let file: AgentFile = serde_json::from_value(file_data.clone())?; + + print_!("File details: {:?}", file); + + Ok(file) + } + + pub async fn retrieve_file_content( + client: &Client, + headers: &HeaderMap, + file_id: &str, + ) -> Result { + let payload = json!({ + "file_id": file_id, + }); + + let route = &format!("files/{}/content", BASE_BETA_URL); + + let response = client + .get(route) + .headers(headers.clone()) + .json(&payload) + .send() + .await?; + + if response.status().is_success() { + let file_string = response.text().await?; + + // let json_value = response.json::().await?; + print_!("File content: {:?}", file_string); + + Ok(file_string) + } else { + print_!("API Err on route {}", route); + // If not successful, perhaps you want to parse it differently or handle the error + Err(anyhow!(response.status())) + } + } +} + +pub async fn upload_file( + client: &Client, + headers: &HeaderMap, + file_path: &Path, + purpose: &str, +) -> Result { + // Open file asynchronously + let file = TokioFile::open(file_path).await?; + let filename = file_path.file_name().unwrap().to_str().unwrap(); + + let reader = FramedRead::new(file, BytesCodec::new()); + let stream = reader.map_ok(|bytes| bytes.freeze()); + + let part = + reqwest::multipart::Part::stream(Body::wrap_stream(stream)).file_name(filename.to_owned()); + + let form = reqwest::multipart::Form::new() + .text("purpose", purpose.to_string()) + .part("file", part); + + // Create and send the request + let response = client + .post("https://api.openai.com/v1/files") + .headers(headers.clone()) + .multipart(form) + .send() + .await?; + + if response.status().is_success() { + let json_value = response.json::().await?; + print_!("JSON Body: {:?}", json_value); + + Ok(json_value) + } else { + let status = response.status(); + eprintln!("API Error with status {} on file upload.", status); + + if let Ok(text) = response.text().await { + eprintln!("Error details: {}", text); + } + + // If not successful, perhaps you want to parse it differently or handle the error + Err(anyhow!(status)) + } +} diff --git a/crates/oai/src/models/assistant/message.rs b/crates/oai/src/models/assistant/message.rs new file mode 100644 index 0000000..cdb2a3a --- /dev/null +++ b/crates/oai/src/models/assistant/message.rs @@ -0,0 +1,108 @@ +use anyhow::Result; +use reqwest::{header::HeaderMap, Client}; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; + +use crate::print_; +use crate::{models::get_data, utils::get_api}; + +use super::ThreadID; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Message { + pub created_at: i64, + pub id: String, + pub object: String, + pub thread_id: String, + pub role: String, + pub content: Vec, + pub file_ids: Vec, // TODO: Figure out what best type to use --> [file.id] +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ContentBlock { + #[serde(rename = "type")] + pub content_type: String, // e.g., "text" + pub text: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct TextBlock { + pub value: String, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub annotations: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] // This allows for the proper variant to be inferred during deserialization +pub enum Annotation { + #[serde(rename = "file_citation")] + FileCitation(FileCitation), + + #[serde(rename = "file_path")] + FilePath(FilePath), +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct FileCitation { + pub file_id: String, + pub quote: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct FilePath { + pub file_id: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Messages(Vec); + +impl Deref for Messages { + type Target = [Message]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Messages { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl AsRef> for Messages { + fn as_ref(&self) -> &Vec { + &self.0 + } +} + +impl AsRef<[Message]> for Messages { + fn as_ref(&self) -> &[Message] { + &self.0 + } +} + +impl Messages { + pub async fn get_messages( + client: &Client, + headers: &HeaderMap, + thread_id: &ThreadID, + ) -> Result { + let response_body = get_api( + client, + headers, + &format!("threads/{}/messages", thread_id), + None, + ) + .await?; + + let msgs = get_data(&response_body)?; + + let messages: Messages = serde_json::from_value(msgs.clone())?; + + print_!("List of Messages: {:?}", messages); + + Ok(messages) + } +} diff --git a/crates/oai/src/models/assistant/mod.rs b/crates/oai/src/models/assistant/mod.rs new file mode 100644 index 0000000..930720e --- /dev/null +++ b/crates/oai/src/models/assistant/mod.rs @@ -0,0 +1,220 @@ +use anyhow::{anyhow, Result}; +use reqwest::{ + header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}, + Client, +}; +use serde::{ + de::{self, Visitor}, + Deserialize, Deserializer, Serialize, +}; +use serde_json::Value; +use std::{collections::HashMap, env, fmt}; + +use self::{ + assistant::{Assistant, Tool}, + message::{Message, Messages}, + run::Run, + thread::Thread, +}; +use serde::Serializer; + +pub mod assistant; +pub mod file; +pub mod message; +pub mod run; +pub mod thread; + +#[derive(Debug, Default)] +pub struct CustomGPT { + pub(crate) client: Client, + pub(crate) headers: HeaderMap, + pub assistants: HashMap, + pub threads: HashMap, +} + +pub type AssistantID = String; +pub type ThreadID = String; +pub type FileID = String; + +impl CustomGPT { + pub fn new() -> Result { + let api_key = env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set in .env file"); + + let client = Client::new(); + let headers = get_auth_headers(&api_key)?; + + Ok(Self { + client, + headers, + ..Default::default() + }) + } + + pub async fn create_assistant( + &mut self, + name: &str, + instructions: &str, + tools: Vec, + ) -> Result { + let assistant = Assistant::create_assistant( + &self.client, + &self.headers, + name.to_string(), + instructions.to_string(), + tools, + OpenAIModels::Gpt41106Preview, + ) + .await?; + + let assistant_id = assistant.id.clone(); + + self.assistants.insert(assistant_id.clone(), assistant); + + Ok(assistant_id) + } + + pub async fn create_thread(&mut self) -> Result { + let thread = Thread::create_thread(&self.client, &self.headers).await?; + let thread_id = thread.inner.id.clone(); + + self.threads.insert(thread_id.clone(), thread); + + Ok(thread_id) + } + + pub async fn add_message_to_thread( + &self, + thread_id: &ThreadID, + content: String, + ) -> Result { + let thread = self + .threads + .get(thread_id) + .ok_or_else(|| anyhow!(format!("No thread found for ID: {:?}", thread_id)))?; + + let message = + Thread::add_message_to_thread(&thread, &self.client, &self.headers, content).await?; + + Ok(message) + } + + pub async fn run(&self, thread_id: &ThreadID, assistant_id: &AssistantID) -> Result { + let run = Run::run(&self.client, &self.headers, thread_id, assistant_id).await?; + + Ok(run) + } + + pub async fn get_messages(&self, thread_id: &ThreadID) -> Result { + let messages = Messages::get_messages(&self.client, &self.headers, thread_id).await?; + + Ok(messages) + } +} + +// Updated get_auth_headers function to accept the API key as a parameter +fn get_auth_headers(api_key: &str) -> Result { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + let auth_value = format!("Bearer {}", api_key); + headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?); + headers.insert("OpenAI-Beta", HeaderValue::from_static("assistants=v1")); + Ok(headers) +} + +#[derive(Debug, Clone, Copy)] +pub enum OpenAIModels { + Gpt432k, + Gpt4, + Gpt35Turbo, + Gpt35Turbo16k, + Gpt35Turbo1106, + Gpt41106Preview, +} + +impl Default for OpenAIModels { + fn default() -> Self { + OpenAIModels::Gpt35Turbo16k + } +} + +impl OpenAIModels { + pub fn new(model: String) -> Self { + let model = match model.as_str() { + "gpt-4-32k" => OpenAIModels::Gpt432k, + "gpt-4" => OpenAIModels::Gpt4, + "gpt-3.5-turbo" => OpenAIModels::Gpt35Turbo, + "gpt-3.5-turbo-16k" => OpenAIModels::Gpt35Turbo16k, + "gpt-3.5-turbo-1106" => OpenAIModels::Gpt35Turbo1106, + "gpt-4-1106-preview" => OpenAIModels::Gpt41106Preview, + _ => panic!("Invalid model {}", model), + }; + + model + } + + pub fn as_string(&self) -> String { + match self { + OpenAIModels::Gpt432k => String::from("gpt-4-32k"), + OpenAIModels::Gpt4 => String::from("gpt-4"), + OpenAIModels::Gpt35Turbo => String::from("gpt-3.5-turbo"), + OpenAIModels::Gpt35Turbo16k => String::from("gpt-3.5-turbo-16k"), + OpenAIModels::Gpt35Turbo1106 => String::from("gpt-3.5-turbo-1106"), + OpenAIModels::Gpt41106Preview => String::from("gpt-4-1106-preview"), + } + } +} + +impl Serialize for OpenAIModels { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let model_str = self.as_string(); + serializer.serialize_str(&model_str) + } +} + +impl<'de> Deserialize<'de> for OpenAIModels { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct OpenAIModelsVisitor; + + impl<'de> Visitor<'de> for OpenAIModelsVisitor { + type Value = OpenAIModels; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string representing an OpenAI model") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + "gpt-4-32k" => Ok(OpenAIModels::Gpt432k), + "gpt-4" => Ok(OpenAIModels::Gpt4), + "gpt-3.5-turbo" => Ok(OpenAIModels::Gpt35Turbo), + "gpt-3.5-turbo-16k" => Ok(OpenAIModels::Gpt35Turbo16k), + "gpt-3.5-turbo-1106" => Ok(OpenAIModels::Gpt35Turbo1106), + "gpt-4-1106-preview" => Ok(OpenAIModels::Gpt41106Preview), + _ => Err(E::custom(format!("unexpected OpenAI model: {}", value))), + } + } + } + + deserializer.deserialize_str(OpenAIModelsVisitor) + } +} + +pub fn get_data(response_body: &Value) -> Result<&Value> { + let data = response_body.get("data").ok_or_else(|| { + anyhow!(format!( + "Unable to find field \"data\" in response_body: {}", + response_body + )) + })?; + + Ok(data) +} diff --git a/crates/oai/src/models/assistant/run.rs b/crates/oai/src/models/assistant/run.rs new file mode 100644 index 0000000..c5740c4 --- /dev/null +++ b/crates/oai/src/models/assistant/run.rs @@ -0,0 +1,255 @@ +use anyhow::Result; +use reqwest::{header::HeaderMap, Client}; +use serde::{ + de::{self, Visitor}, + Deserialize, Deserializer, +}; +use serde_json::json; +use std::{collections::HashMap, fmt, time::Duration}; +use tokio::time::sleep; + +use super::{assistant::Tool, get_data, AssistantID, OpenAIModels, ThreadID}; +use crate::print_; +use crate::utils::{get_api, post_api}; + +#[derive(Deserialize, Debug)] +pub struct Run { + pub id: String, + pub object: String, + pub created_at: u32, + pub assistant_id: String, + pub thread_id: String, + pub status: RunStatus, + pub started_at: Option, + pub expires_at: Option, + pub cancelled_at: Option, + pub failed_at: Option, + pub completed_at: Option, + pub last_error: Option, + pub model: OpenAIModels, + pub instructions: Option, + pub tools: Vec, + pub file_ids: Vec, + pub metadata: HashMap, +} + +impl Run { + pub async fn create_run( + client: &Client, + headers: &HeaderMap, + thread_id: &ThreadID, + assistant_id: &AssistantID, + ) -> Result { + let payload = json!({ + "assistant_id": assistant_id, + }); + + let response_body = post_api( + client, + headers, + &format!("threads/{}/runs", thread_id), + &payload, + ) + .await?; + + let run: Run = serde_json::from_value(response_body)?; + + print_!("Run: {:?}", run); + + Ok(run) + } +} + +impl Run { + pub async fn run( + client: &Client, + headers: &HeaderMap, + thread_id: &ThreadID, + assistant_id: &AssistantID, + ) -> Result { + let mut run = Run::create_run(client, headers, thread_id, assistant_id).await?; + + // Poll the run status until it is completed or failed + while !run.is_completed() && !run.is_failed() { + sleep(Duration::from_millis(500)).await; + + print_!("Checking in if Run is completed"); + run = run.get_run(client, headers, &run.id).await?; + } + + print_!("Run has been completed."); + + Ok(run) + } + + async fn get_run(&self, client: &Client, headers: &HeaderMap, run_id: &str) -> Result { + let response_body = get_api( + client, + headers, + &format!("threads/{}/runs/{}", self.thread_id, run_id), + None, + ) + .await?; + + serde_json::from_value(response_body).map_err(Into::into) + } + + /// curl https://api.openai.com/v1/threads/thread_nrrMnuRNH45pTs00DEymPqol/runs \ + /// -H 'Authorization: Bearer ' \ + /// -H 'Content-Type: application/json' \ + /// -H 'OpenAI-Beta: assistants=v1' + pub async fn list_runs( + client: &Client, + headers: &HeaderMap, + thread_id: &ThreadID, + ) -> Result> { + let response_body = get_api( + client, + headers, + &format!("threads/{}/runs", thread_id), + None, + ) + .await?; + + let runs = get_data(&response_body)?; + + serde_json::from_value(runs.clone()).map_err(Into::into) + } +} + +impl Run { + fn is_completed(&self) -> bool { + self.status == RunStatus::Completed + } + + fn is_failed(&self) -> bool { + self.status == RunStatus::Failed + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum RunStatus { + Queued, + InProgress, + RequiresAction, + Cancelling, + Failed, + Completed, + Expired, +} + +impl<'de> Deserialize<'de> for RunStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct RunStatusVisitor; + + impl<'de> Visitor<'de> for RunStatusVisitor { + type Value = RunStatus; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string representing an RunStatus") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + "queued" => Ok(RunStatus::Queued), + "in_progress" => Ok(RunStatus::InProgress), + "requires_action" => Ok(RunStatus::RequiresAction), + "cancelling" => Ok(RunStatus::Cancelling), + "failed" => Ok(RunStatus::Failed), + "completed" => Ok(RunStatus::Completed), + "expired" => Ok(RunStatus::Expired), + _ => Err(E::custom(format!("unexpected RunStatus value: {}", value))), + } + } + } + + deserializer.deserialize_str(RunStatusVisitor) + } +} + +#[derive(Deserialize, Debug)] +pub struct RunErr { + pub code: RunErrStatus, + pub message: String, +} + +#[derive(Debug)] +pub enum RunErrStatus { + ServerError, + RateLimitExceeded, +} + +impl<'de> Deserialize<'de> for RunErrStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct RunErrStatusVisitor; + + impl<'de> Visitor<'de> for RunErrStatusVisitor { + type Value = RunErrStatus; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string representing an RunErr status") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + "server_error" => Ok(RunErrStatus::ServerError), + "rate_limit_exceeded" => Ok(RunErrStatus::RateLimitExceeded), + _ => Err(E::custom(format!("unexpected RunErr status: {}", value))), + } + } + } + + deserializer.deserialize_str(RunErrStatusVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialization() -> Result<()> { + let json_data = json!({ + "assistant_id": "asst_QZG3jl4N01Enuh8KacwSjmSQ", + "cancelled_at": null, + "completed_at": null, + "created_at": 1699460132, + "expires_at": 1699460732, + "failed_at": null, + "file_ids": [], + "id": "run_yQgBbVzMz8nMFaSiIR9pjyx3", + "instructions": "You are a personal math tutor. Write and run code to answer math questions.", + "last_error": null, + "metadata": {}, + "model": "gpt-4-1106-preview", + "object": "thread.run", + "started_at": null, + "status": "queued", + "thread_id": "thread_D6wrSRmIE1k1MyEe4S7fdFHs", + "tools": [{"type": "code_interpreter"}] + }) + .to_string(); + + let run: Run = serde_json::from_str(&json_data)?; + + assert_eq!(run.id, "run_yQgBbVzMz8nMFaSiIR9pjyx3"); + assert_eq!(run.assistant_id, "asst_QZG3jl4N01Enuh8KacwSjmSQ"); + assert!(matches!(run.status, RunStatus::Queued)); + assert_eq!(run.tools.len(), 1); + assert!(matches!(run.tools[0], Tool::CodeInterpreter)); + + Ok(()) + } +} diff --git a/crates/oai/src/models/assistant/thread.rs b/crates/oai/src/models/assistant/thread.rs new file mode 100644 index 0000000..5d6cdbe --- /dev/null +++ b/crates/oai/src/models/assistant/thread.rs @@ -0,0 +1,69 @@ +use anyhow::Result; +use reqwest::{header::HeaderMap, Client}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::collections::HashMap; + +use super::message::{Message, Messages}; +use crate::print_; +use crate::utils::post_api; + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Thread { + pub inner: ThreadInner, + pub messages: Vec, + pub msg_ids: HashMap, +} + +pub type MessageID = String; +pub type MessageIndex = u32; + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct ThreadInner { + pub id: String, + pub object: String, + pub created_at: u32, // Use chrono::DateTime, + pub metadata: HashMap, +} + +impl Thread { + pub async fn create_thread(client: &Client, headers: &HeaderMap) -> Result { + let payload = json!({}); + + let response_body = post_api(client, headers, "threads", &payload).await?; + let inner: ThreadInner = serde_json::from_value(response_body)?; + + print_!("Thread: {:?}", inner); + + Ok(Thread { + inner, + ..Default::default() + }) + } + + pub async fn add_message_to_thread( + &self, + client: &Client, + headers: &HeaderMap, + content: String, + ) -> Result { + let payload = json!({ + "role": "user", + "content": content, + }); + + let response_body = post_api( + client, + headers, + &format!("threads/{}/messages", self.inner.id), + &payload, + ) + .await?; + + let message: Message = serde_json::from_value(response_body)?; + + print_!("Message: {:?}", message); + + Ok(message) + } +} diff --git a/crates/oai/src/models/chat/client.rs b/crates/oai/src/models/chat/client.rs deleted file mode 100644 index e69de29..0000000 diff --git a/crates/oai/src/models/chat/mod.rs b/crates/oai/src/models/chat/mod.rs index eaaf55b..9911ea7 100644 --- a/crates/oai/src/models/chat/mod.rs +++ b/crates/oai/src/models/chat/mod.rs @@ -1,3 +1,212 @@ pub mod params; pub mod request; pub mod response; + +#[cfg(not(feature = "wasm"))] +use reqwest::header::HeaderMap; + +#[cfg(feature = "wasm")] +use crate::foreign::{IMessagesData, IModelsData}; +#[cfg(feature = "wasm")] +use js_sys::{Date as IDate, JsString}; +#[cfg(feature = "wasm")] +use wasmer::{JsError, WasmType}; + +use super::{message::GptMessage, model::ModelData}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Chat { + #[serde(skip)] + #[cfg(not(feature = "wasm"))] + pub(crate) headers: HeaderMap, + pub session_id: String, + pub title: String, + pub models: HashMap, + pub messages: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct MessageData { + pub user: String, + pub ts: DateTime, + pub payload: GptMessage, +} + +impl Chat { + pub fn new( + #[cfg(not(feature = "wasm"))] headers: HeaderMap, + session_id: String, + title: String, + ) -> Chat { + Self { + #[cfg(not(feature = "wasm"))] + headers, + session_id, + title, + models: HashMap::new(), + messages: Vec::new(), + } + } +} + +/// === WASM === + +#[cfg(feature = "wasm")] +pub mod wasm { + use super::*; + use crate::models::{ + message::wasm::GptMessageWasm, + model::{wasm::ModelDataWasm, GptModel}, + }; + use anyhow::Result; + use derive_more::{AsRef, Deref, DerefMut}; + use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + + #[wasm_bindgen] + #[derive(Debug, Deserialize, Serialize, Clone, AsRef, Deref, DerefMut)] + #[serde(rename_all = "camelCase", rename = "Chat")] + pub struct ChatWasm(Chat); + + #[wasm_bindgen] + #[derive(Debug, Deserialize, Serialize, Clone, AsRef, Deref, DerefMut)] + #[serde(rename_all = "camelCase", rename = "Message")] + pub struct MessageDataWasm(pub(crate) MessageData); + + #[wasm_bindgen] + impl ChatWasm { + #[wasm_bindgen(constructor)] + pub fn new(session_id: String, title: String) -> ChatWasm { + // The headers are not used in WASM so it's dummy + Self(Chat::new( + #[cfg(not(feature = "wasm"))] + HeaderMap::new(), + session_id, + title, + )) + } + + #[wasm_bindgen(getter, js_name = sessionId)] + pub fn session_id(&self) -> JsString { + self.session_id.clone().into() + } + + #[wasm_bindgen(getter)] + pub fn title(&self) -> JsString { + self.title.clone().into() + } + + #[wasm_bindgen(getter)] + pub fn models(&self) -> Result { + let map = self + .models + .iter() + .map(|(k, model)| (k.clone(), ModelDataWasm(model.clone()))) // automatically dereferences + .collect(); + + HashMap::to_extern(map) + } + + #[wasm_bindgen(getter)] + pub fn messages(&self) -> Result { + let msgs = self + .messages + .iter() + .map(|msg| MessageDataWasm(msg.clone())) + .collect(); + + Vec::to_extern(msgs) + } + + #[wasm_bindgen(js_name = addMessage)] + pub fn add_message(&mut self, message: MessageDataWasm) { + let MessageDataWasm(msg_data) = message; + self.messages.push(msg_data); + } + + #[wasm_bindgen(js_name = setMessages)] + pub fn set_messages( + &mut self, + messages: IMessagesData, + ) -> Result<(), JsError> { + let mut messages: Vec = + Vec::from_extern(messages)?; + + let messages: Vec = messages + .drain(..) + .map(|msg| { + let MessageDataWasm(msg_data) = msg; + msg_data + }) + .collect(); + + self.messages = messages; + + Ok(()) + } + + #[wasm_bindgen(js_name = addModel)] + pub fn add_model(&mut self, model: GptModel) -> Result<(), JsError> { + let model_id = model.as_str(); + + self.models.insert( + model_id.to_string(), + ModelData::new(model_id.to_string()), + ); + + Ok(()) + } + + #[wasm_bindgen(js_name = castFromString)] + pub fn cast_from_string(json: String) -> Result { + let chat = serde_json::from_str(&json) + .map_err(|e| JsError::from_str(&e.to_string()))?; + + Ok(chat) + } + + #[wasm_bindgen(js_name = castToString)] + pub fn cast_to_string(&self) -> Result { + let json = serde_json::to_string_pretty(self) + .map_err(|e| JsError::from_str(&e.to_string()))?; + + Ok(json.into()) + } + } + + #[wasm_bindgen] + impl MessageDataWasm { + #[wasm_bindgen(constructor)] + pub fn new( + user: String, + ts: IDate, + payload: GptMessageWasm, + ) -> Result { + let datetime = DateTime::from_extern(ts.into())?; + + let GptMessageWasm(payload) = payload; + + Ok(MessageDataWasm(MessageData { + user, + ts: datetime, + payload, + })) + } + + #[wasm_bindgen(getter)] + pub fn user(&self) -> JsString { + self.user.clone().into() + } + + #[wasm_bindgen(getter)] + pub fn ts(&self) -> Result { + DateTime::to_extern(self.ts.clone().into()) + } + + #[wasm_bindgen(getter)] + pub fn payload(&self) -> GptMessageWasm { + GptMessageWasm(self.payload.clone()) + } + } +} diff --git a/crates/oai/src/models/chat/params.rs b/crates/oai/src/models/chat/params.rs index f7d2904..fd5a218 100644 --- a/crates/oai/src/models/chat/params.rs +++ b/crates/oai/src/models/chat/params.rs @@ -3,13 +3,13 @@ use serde::Serialize; use std::collections::HashMap; use crate::{ - models::Models, + models::model::GptModel, utils::{Bounded, BoundedFloat, Scale01, Scale100s, Scale22}, }; #[derive(Debug, Serialize, Clone)] pub struct ChatParams { - pub model: Models, + pub model: GptModel, // TODO: THIS SHOULD BE Scale02 /// Temperature is used to control the randomness or creativity /// of the model's output. Temperature is a parameter that affects @@ -74,7 +74,7 @@ pub struct ChatParams { impl ChatParams { pub fn new( - model: Models, + model: GptModel, temperature: Option, max_tokens: Option, top_p: Option, @@ -118,7 +118,7 @@ impl ChatParams { }) } - pub fn empty(model: Models) -> Self { + pub fn empty(model: GptModel) -> Self { Self { model, temperature: None, @@ -175,7 +175,7 @@ impl ChatParams { self } - pub fn default_with_model(model: Models) -> Self { + pub fn default_with_model(model: GptModel) -> Self { Self { model, temperature: None, @@ -209,7 +209,7 @@ pub mod wasm { impl ChatParamsWasm { #[wasm_bindgen(constructor)] pub fn new( - model: Models, + model: GptModel, temperature: Option, max_tokens: Option, top_p: Option, @@ -242,7 +242,7 @@ pub mod wasm { } #[wasm_bindgen] - pub fn empty(model: Models) -> Self { + pub fn empty(model: GptModel) -> Self { ChatParamsWasm(ChatParams::empty(model)) } diff --git a/crates/oai/src/models/chat/request.rs b/crates/oai/src/models/chat/request.rs index 5b3224f..efb945b 100644 --- a/crates/oai/src/models/chat/request.rs +++ b/crates/oai/src/models/chat/request.rs @@ -9,22 +9,17 @@ use crate::models::{ #[cfg(not(feature = "wasm"))] pub mod native { - use super::*; + use super::{super::Chat, *}; use bytes::Bytes; use futures::Stream; + use reqwest::Client; use std::{ops::Deref, time::Duration}; - use reqwest::{header::HeaderMap, Client}; - - pub struct ChatClient { - pub(crate) headers: HeaderMap, - } - - impl ChatClient { + impl Chat { pub async fn chat( &self, job: impl Deref, - msgs: &[&Message], + msgs: &[&GptMessage], funcs: &[&String], stop_seq: &[String], ) -> Result { @@ -57,8 +52,9 @@ pub mod native { } pub async fn chat_stream( + &self, job: &ChatParams, - msgs: &[&Message], + msgs: &[&GptMessage], funcs: &[&String], stop_seq: &[String], ) -> Result>> { @@ -75,7 +71,7 @@ pub mod native { Duration::from_secs(5), client .post("https://api.openai.com/v1/chat/completions") - .headers(headers.clone()) + .headers(self.headers.clone()) .json(&req_body) .send(), ) @@ -108,7 +104,7 @@ pub mod native { #[cfg(feature = "wasm")] pub mod wasm { use crate::{ - foreign::IMessages, + foreign::IGptMessage, models::{ chat::params::wasm::ChatParamsWasm, message::wasm::GptMessageWasm, }, @@ -191,7 +187,7 @@ pub mod wasm { #[wasm_bindgen(js_name = requestBody)] pub fn request_body_wasm( - msgs: IMessages, + msgs: IGptMessage, job: ChatParamsWasm, stream: bool, ) -> Result { diff --git a/crates/oai/src/models/message.rs b/crates/oai/src/models/message.rs index 2ff0c19..53ec4ca 100644 --- a/crates/oai/src/models/message.rs +++ b/crates/oai/src/models/message.rs @@ -47,7 +47,7 @@ pub mod wasm { #[wasm_bindgen(js_name = "Message")] #[derive(Debug, Clone)] - pub struct GptMessageWasm(GptMessage); + pub struct GptMessageWasm(pub(crate) GptMessage); #[wasm_bindgen] impl GptMessageWasm { diff --git a/crates/oai/src/models/mod.rs b/crates/oai/src/models/mod.rs index a962e6e..50de9d2 100644 --- a/crates/oai/src/models/mod.rs +++ b/crates/oai/src/models/mod.rs @@ -1,43 +1,4 @@ pub mod chat; pub mod message; pub mod role; - -use ::anyhow::Result; -use serde::{Deserialize, Serialize}; - -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::wasm_bindgen; - -#[cfg_attr(feature = "wasm", wasm_bindgen)] -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub enum Models { - #[serde(rename = "gpt-4-32k")] - Gpt432k, - #[serde(rename = "gpt-4")] - Gpt4, - #[serde(rename = "gpt-3.5-turbo")] - Gpt35Turbo, - #[serde(rename = "gpt-3.5-turbo-16k")] - Gpt35Turbo16k, - #[serde(rename = "gpt-3.5-turbo-1106")] - Gpt35Turbo1106, - #[serde(rename = "gpt-4-1106-preview")] - Gpt41106Preview, -} - -impl Models { - pub fn from_str(file_purpose: &str) -> Result { - serde_json::from_str(file_purpose).map_err(Into::into) - } - - pub fn as_str(&self) -> &str { - match self { - Models::Gpt432k => "gpt-4-32k", - Models::Gpt4 => "gpt-4", - Models::Gpt35Turbo => "gpt-3.5-turbo", - Models::Gpt35Turbo16k => "gpt-3.5-turbo-16k", - Models::Gpt35Turbo1106 => "gpt-3.5-turbo-1106", - Models::Gpt41106Preview => "gpt-4-1106-preview", - } - } -} +pub mod model; diff --git a/crates/oai/src/models/model.rs b/crates/oai/src/models/model.rs new file mode 100644 index 0000000..72fb854 --- /dev/null +++ b/crates/oai/src/models/model.rs @@ -0,0 +1,93 @@ +use ::anyhow::Result; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum GptModel { + #[serde(rename = "gpt-4-32k")] + Gpt432k, + #[serde(rename = "gpt-4")] + Gpt4, + #[serde(rename = "gpt-3.5-turbo")] + Gpt35Turbo, + #[serde(rename = "gpt-3.5-turbo-16k")] + Gpt35Turbo16k, + #[serde(rename = "gpt-3.5-turbo-1106")] + Gpt35Turbo1106, + #[serde(rename = "gpt-4-1106-preview")] + Gpt41106Preview, +} + +impl GptModel { + pub fn from_str(file_purpose: &str) -> Result { + serde_json::from_str(file_purpose).map_err(Into::into) + } + + pub fn as_str(&self) -> &str { + match self { + GptModel::Gpt432k => "gpt-4-32k", + GptModel::Gpt4 => "gpt-4", + GptModel::Gpt35Turbo => "gpt-3.5-turbo", + GptModel::Gpt35Turbo16k => "gpt-3.5-turbo-16k", + GptModel::Gpt35Turbo1106 => "gpt-3.5-turbo-1106", + GptModel::Gpt41106Preview => "gpt-4-1106-preview", + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase", rename = "Model")] +pub struct ModelData { + pub id: String, + pub uri: String, + pub interface: String, +} + +impl ModelData { + pub fn new(id: String) -> ModelData { + Self { + id, + // TODO: Should not be hardcoded + uri: String::from("https://api.openai.com/v1/chat/completions"), + interface: String::from("OpenAI"), + } + } +} + +#[cfg(feature = "wasm")] +pub mod wasm { + use super::*; + use derive_more::{AsRef, Deref, DerefMut}; + use js_sys::JsString; + + #[wasm_bindgen] + #[derive(Debug, Deserialize, Serialize, Clone, AsRef, Deref, DerefMut)] + #[serde(rename_all = "camelCase", rename = "Model")] + pub struct ModelDataWasm(pub(crate) ModelData); + + #[wasm_bindgen] + impl ModelDataWasm { + #[wasm_bindgen(constructor)] + pub fn new(id: String) -> ModelDataWasm { + ModelDataWasm(ModelData::new(id)) + } + + #[wasm_bindgen(getter)] + pub fn id(&self) -> JsString { + self.id.clone().into() + } + + #[wasm_bindgen(getter)] + pub fn uri(&self) -> JsString { + self.uri.clone().into() + } + + #[wasm_bindgen(getter)] + pub fn interface(&self) -> JsString { + self.interface.clone().into() + } + } +} From d9f134f1aab1ac93e9fcf416dcaf0b64a564ad7d Mon Sep 17 00:00:00 2001 From: Nuno Boavida <45330362+nmboavida@users.noreply.github.com> Date: Wed, 15 Nov 2023 23:07:28 +0000 Subject: [PATCH 6/6] Fix VSCE build --- README.md | 10 +++++++ bin/create_interface.py | 2 +- crates/oai/src/lib.rs | 2 +- crates/oai/src/models/chat/params.rs | 36 ++++++----------------- crates/oai/src/models/message.rs | 11 +++---- crates/oai/src/models/model.rs | 1 - vsce/src/chat/commands.ts | 8 ++--- vsce/src/chat/handlers.ts | 4 +-- vsce/src/chat/webview.ts | 2 +- vsce/src/core/appData.ts | 20 ++++++------- vsce/src/core/workflows/initCodeBase.ts | 2 +- vsce/src/taskPool/commands/retryTask.ts | 2 +- vsce/src/taskPool/commands/runAllTasks.ts | 4 +-- vsce/src/taskPool/commands/runTask.ts | 2 +- vsce/src/utils/utils.ts | 32 ++++++++++---------- webview/src/ components/chatContainer.tsx | 2 +- webview/src/ components/chatStream.tsx | 2 +- webview/src/ components/vsceClient.tsx | 2 +- webview/wasm/neatcoderInterface.d.ts | 33 +++++++++------------ 19 files changed, 79 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index eaa6792..8406362 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,13 @@ Install VS extension: Copy `tasks.json` and `launch.json` from vscode to .vscode folder + + + +## To compile oai crate + +As default native: +`cargo build` + +As a Wasm Lib: +`cargo build --target wasm32-unknown-unknown --features wasm --no-default-features` diff --git a/bin/create_interface.py b/bin/create_interface.py index 5e100f4..70d6a77 100644 --- a/bin/create_interface.py +++ b/bin/create_interface.py @@ -27,7 +27,7 @@ def process_file(input_filename, output_filename): # Check for methods by looking for lines with parentheses and ignore them if "(" in stripped_line or ")" in stripped_line: continue - elif stripped_line.endswith(";"): + elif stripped_line.endswith(";"): ## Can either be a method or readonly field # If it's a readonly field, remove the readonly keyword # Ensure indentation when adding to output output_lines.append(" " + stripped_line.replace("readonly ", "")) diff --git a/crates/oai/src/lib.rs b/crates/oai/src/lib.rs index 1e7bfbd..4ed5e82 100644 --- a/crates/oai/src/lib.rs +++ b/crates/oai/src/lib.rs @@ -18,7 +18,7 @@ pub mod foreign { #[wasm_bindgen] extern "C" { - #[wasm_bindgen(typescript_type = "Array")] + #[wasm_bindgen(typescript_type = "Array")] pub type IGptMessage; } } diff --git a/crates/oai/src/models/chat/params.rs b/crates/oai/src/models/chat/params.rs index fd5a218..52c5d7a 100644 --- a/crates/oai/src/models/chat/params.rs +++ b/crates/oai/src/models/chat/params.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use crate::{ @@ -7,7 +7,7 @@ use crate::{ utils::{Bounded, BoundedFloat, Scale01, Scale100s, Scale22}, }; -#[derive(Debug, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct ChatParams { pub model: GptModel, // TODO: THIS SHOULD BE Scale02 @@ -195,16 +195,18 @@ impl ChatParams { #[cfg(feature = "wasm")] pub mod wasm { - use std::ops::{Deref, DerefMut}; - use super::*; - + use derive_more::{AsRef, Deref, DerefMut}; + use serde::Deserialize; use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; use wasmer::{jsvalue_to_hmap, JsError}; - #[wasm_bindgen(js_name = "ChatParams")] - pub struct ChatParamsWasm(ChatParams); + #[wasm_bindgen] + #[derive(Debug, Deserialize, Serialize, Clone, AsRef, Deref, DerefMut)] + #[serde(rename_all = "camelCase", rename = "ChatPArams")] + pub struct ChatParamsWasm(pub(crate) ChatParams); + // TODO: Add getters #[wasm_bindgen] impl ChatParamsWasm { #[wasm_bindgen(constructor)] @@ -294,24 +296,4 @@ pub mod wasm { self } } - - impl AsRef for ChatParamsWasm { - fn as_ref(&self) -> &ChatParams { - &self.0 - } - } - - impl Deref for ChatParamsWasm { - type Target = ChatParams; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl DerefMut for ChatParamsWasm { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } } diff --git a/crates/oai/src/models/message.rs b/crates/oai/src/models/message.rs index 53ec4ca..e2bcddc 100644 --- a/crates/oai/src/models/message.rs +++ b/crates/oai/src/models/message.rs @@ -35,17 +35,14 @@ impl GptMessage { #[cfg(feature = "wasm")] pub mod wasm { - use std::ops::{Deref, DerefMut}; - + use super::GptMessage; + use crate::models::role::Role; use js_sys::JsString; use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::ops::{Deref, DerefMut}; use wasm_bindgen::prelude::wasm_bindgen; - use crate::models::role::Role; - - use super::GptMessage; - - #[wasm_bindgen(js_name = "Message")] + #[wasm_bindgen] #[derive(Debug, Clone)] pub struct GptMessageWasm(pub(crate) GptMessage); diff --git a/crates/oai/src/models/model.rs b/crates/oai/src/models/model.rs index 72fb854..82a1b7a 100644 --- a/crates/oai/src/models/model.rs +++ b/crates/oai/src/models/model.rs @@ -39,7 +39,6 @@ impl GptModel { } #[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase", rename = "Model")] pub struct ModelData { pub id: String, pub uri: String, diff --git a/vsce/src/chat/commands.ts b/vsce/src/chat/commands.ts index 1b6c6bd..256afa9 100644 --- a/vsce/src/chat/commands.ts +++ b/vsce/src/chat/commands.ts @@ -45,7 +45,7 @@ export async function initChat( let modelVersion = await getOrSetModelVersion(); const sessionId = uuidv4(); - const chat = new wasm.Chat(sessionId, "Chat with Neat"); + const chat = new wasm.ChatWasm(sessionId, "Chat with Neat"); chat.addModel(modelVersion!); // This will add the storeChat operation to the queue to be processed in order. @@ -123,7 +123,7 @@ export async function openChat( const setupWebviewSockets = async ( message: any, panel: vscode.WebviewPanel, - chat: wasm.Chat + chat: wasm.ChatWasm ) => { switch (message.command) { case "promptLLM": @@ -131,7 +131,7 @@ const setupWebviewSockets = async ( // panel so it knows which panel sent the message chat.setMessages(message.msgs); // TODO: Move to addMessage to reduce communication overhead chatOperationQueue.add(() => storeChat(chat)); - const msgs: Array = message.msgs; + const msgs: Array = message.msgs; const isFirst = msgs.length === 1 ? true : false; await getOrSetApiKey(); @@ -139,7 +139,7 @@ const setupWebviewSockets = async ( promptLLM(panel, message); if (isFirst) { - await chat.setTitle(makeRequest); + await wasm.setTitle(chat, makeRequest); chatOperationQueue.add(() => storeChat(chat)); // Change the title in the config diff --git a/vsce/src/chat/handlers.ts b/vsce/src/chat/handlers.ts index 479fc7c..03adc64 100644 --- a/vsce/src/chat/handlers.ts +++ b/vsce/src/chat/handlers.ts @@ -9,7 +9,7 @@ import { MessageBuffer } from "../utils/httpClient"; import { getLLMParams } from "../utils/utils"; export async function buildRequest( - msgs: Array, + msgs: Array, stream: boolean ): Promise<[any, any]> { const apiKey = await getOrSetApiKey(); @@ -34,7 +34,7 @@ export async function promptLLM( webviewPanel: vscode.WebviewPanel, message: any ): Promise { - const msgs: Array = message.msgs; + const msgs: Array = message.msgs; const stream = message.stream; const [apiKey, body] = await buildRequest(msgs, stream); diff --git a/vsce/src/chat/webview.ts b/vsce/src/chat/webview.ts index d9d3757..6153a12 100644 --- a/vsce/src/chat/webview.ts +++ b/vsce/src/chat/webview.ts @@ -6,7 +6,7 @@ import * as wasm from "../../pkg/neatcoder"; export function setWebviewContent( panel: vscode.WebviewPanel, context: vscode.ExtensionContext, - chatHistory?: wasm.Chat + chatHistory?: wasm.ChatWasm ) { const reactBuildPath = path.join(context.extensionPath, "webview/build/"); // Assuming 'webview' is where you copied your build files const entryHtml = path.join(reactBuildPath, "index.html"); diff --git a/vsce/src/core/appData.ts b/vsce/src/core/appData.ts index 93468cf..4b1cef2 100644 --- a/vsce/src/core/appData.ts +++ b/vsce/src/core/appData.ts @@ -131,10 +131,10 @@ export class appDataManager { /** * Runs all tasks in the task pool in the application state sequentially. * - * @param {wasm.OpenAIParams} llmParams - The parameters for the OpenAI client. + * @param {wasm.ChatParamsWasm} llmParams - The parameters for the OpenAI client. * @returns {Promise} - A promise indicating the completion of all tasks. */ - public async runAllTasks(llmParams: wasm.OpenAIParams): Promise { + public async runAllTasks(llmParams: wasm.ChatParamsWasm): Promise { try { // Retrieve all tasks from the task pool. const tasks: wasm.Task[] = this.appData.getTodoTasks(); @@ -162,12 +162,12 @@ export class appDataManager { * Initiates a task based on the task ID and the associated task parameters. * * @param {number} taskId - The ID of the task to start. - * @param {wasm.OpenAIParams} llmParams - The parameters for the OpenAI client. + * @param {wasm.ChatParamsWasm} llmParams - The parameters for the OpenAI client. * @returns {Promise} - A promise indicating the completion of the task. */ public async runTask( taskId: number, - llmParams: wasm.OpenAIParams + llmParams: wasm.ChatParamsWasm ): Promise { const task = this.appData.popTodo(taskId); const taskType = task.taskType(); @@ -236,12 +236,12 @@ export class appDataManager { * Retries a task based on the task ID and the associated task parameters. * * @param {number} taskId - The ID of the task to retry. - * @param {wasm.OpenAIParams} llmParams - The parameters for the OpenAI client. + * @param {wasm.ChatParamsWasm} llmParams - The parameters for the OpenAI client. * @returns {Promise} - A promise indicating the completion of the task. */ public async retryTask( taskId: number, - llmParams: wasm.OpenAIParams + llmParams: wasm.ChatParamsWasm ): Promise { const task = this.appData.popDone(taskId); this.appData.addBackTodo(task); @@ -251,10 +251,10 @@ export class appDataManager { /** * Starts a prompt with the given OpenAI client, parameters, and user input. * - * @param {wasm.OpenAIParams} llmParams - The parameters for the OpenAI client. + * @param {wasm.ChatParamsWasm} llmParams - The parameters for the OpenAI client. * @param {string} userInput - The user input to start the prompt with. */ - public async initCodeBase(llmParams: wasm.OpenAIParams, userInput: string) { + public async initCodeBase(llmParams: wasm.ChatParamsWasm, userInput: string) { try { await this.scaffoldProject(llmParams, userInput); saveappDataToFile(this.appData); @@ -272,10 +272,10 @@ export class appDataManager { * Initiates a scaffold project operation using specified OpenAI client, parameters, and user input. * This method creates necessary task parameters and invokes the scaffold project method from the appData object. * - * @param {wasm.OpenAIParams} llmParams - The parameters for the OpenAI client. + * @param {wasm.ChatParamsWasm} llmParams - The parameters for the OpenAI client. * @param {string} userInput - The user input string. */ - async scaffoldProject(llmParams: wasm.OpenAIParams, userInput: string) { + async scaffoldProject(llmParams: wasm.ChatParamsWasm, userInput: string) { const taskType = wasm.TaskType.ScaffoldProject; const taskPayload = new wasm.TaskParamsInner( diff --git a/vsce/src/core/workflows/initCodeBase.ts b/vsce/src/core/workflows/initCodeBase.ts index d355769..b5c13fc 100644 --- a/vsce/src/core/workflows/initCodeBase.ts +++ b/vsce/src/core/workflows/initCodeBase.ts @@ -17,7 +17,7 @@ import MixpanelHelper from "../../utils/mixpanelHelper"; * @returns Promise - A promise that resolves once the prompt process has initiated and the user has been notified. */ export async function initCodeBase( - llmParams: wasm.OpenAIParams, + llmParams: wasm.ChatParamsWasm, appManager: appDataManager ): Promise { { diff --git a/vsce/src/taskPool/commands/retryTask.ts b/vsce/src/taskPool/commands/retryTask.ts index 6a63c2e..808e1d8 100644 --- a/vsce/src/taskPool/commands/retryTask.ts +++ b/vsce/src/taskPool/commands/retryTask.ts @@ -12,7 +12,7 @@ import { appDataManager } from "../../core/appData"; */ export async function retryTask( taskView: TaskView, - llmParams: wasm.OpenAIParams, + llmParams: wasm.ChatParamsWasm, appManager: appDataManager ): Promise { const taskId = taskView.task!.id; diff --git a/vsce/src/taskPool/commands/runAllTasks.ts b/vsce/src/taskPool/commands/runAllTasks.ts index cd590ae..978c1e5 100644 --- a/vsce/src/taskPool/commands/runAllTasks.ts +++ b/vsce/src/taskPool/commands/runAllTasks.ts @@ -9,8 +9,8 @@ import { appDataManager } from "../../core/appData"; * @return Promise - A promise that resolves once the job has been initiated. */ export async function runAllTasks( - llmParams: wasm.OpenAIParams, + llmParams: wasm.ChatParamsWasm, appManager: appDataManager ): Promise { - appManager.runAllTasks(llmParams); + appManager.runAllTasks(llmParams); } diff --git a/vsce/src/taskPool/commands/runTask.ts b/vsce/src/taskPool/commands/runTask.ts index d92eca2..75c7fa8 100644 --- a/vsce/src/taskPool/commands/runTask.ts +++ b/vsce/src/taskPool/commands/runTask.ts @@ -12,7 +12,7 @@ import { appDataManager } from "../../core/appData"; */ export async function runTask( taskView: TaskView, - llmParams: wasm.OpenAIParams, + llmParams: wasm.ChatParamsWasm, appManager: appDataManager ): Promise { const taskId = taskView.task!.id; diff --git a/vsce/src/utils/utils.ts b/vsce/src/utils/utils.ts index 14f3406..5b55f96 100644 --- a/vsce/src/utils/utils.ts +++ b/vsce/src/utils/utils.ts @@ -274,7 +274,7 @@ export function getOrSetApiKey(): Promise { }); } -export async function getOrSetModelVersion(): Promise { +export async function getOrSetModelVersion(): Promise { let config = vscode.workspace.getConfiguration("extension"); let modelVersion = config.get("modelVersion") as string; @@ -332,32 +332,30 @@ export async function setModelVersion() { } } -export function fromModelVersionToEnum( - modelStr: string -): wasm.OpenAIModels | null { +export function fromModelVersionToEnum(modelStr: string): wasm.GptModel | null { switch (modelStr) { case "gpt-4-32k": - return wasm.OpenAIModels.Gpt432k; + return wasm.GptModel.Gpt432k; case "gpt-4": - return wasm.OpenAIModels.Gpt4; + return wasm.GptModel.Gpt4; case "gpt-3.5-turbo": - return wasm.OpenAIModels.Gpt35Turbo; + return wasm.GptModel.Gpt35Turbo; case "gpt-3.5-turbo-16k": - return wasm.OpenAIModels.Gpt35Turbo16k; + return wasm.GptModel.Gpt35Turbo16k; case "gpt-3.5-turbo-1106": - return wasm.OpenAIModels.Gpt35Turbo1106; + return wasm.GptModel.Gpt35Turbo1106; case "gpt-4-1106-preview": - return wasm.OpenAIModels.Gpt41106Preview; + return wasm.GptModel.Gpt41106Preview; default: return null; } } -export async function getChat(uri: vscode.Uri): Promise { +export async function getChat(uri: vscode.Uri): Promise { try { const data = await vscode.workspace.fs.readFile(uri); const content = Buffer.from(data).toString("utf8"); - return wasm.Chat.castFromString(content); + return wasm.ChatWasm.castFromString(content); } catch (err) { console.error("Failed to get chat:", err); throw new Error(`Failed to get chat from ${uri.path}: ${err}`); @@ -365,7 +363,7 @@ export async function getChat(uri: vscode.Uri): Promise { } // The wrapper function that constructs the Uri from the chat ID -export async function getChatById(chatId: string): Promise { +export async function getChatById(chatId: string): Promise { const root = getRoot(); const chatsDir = path.join(root, ".neat", "chats"); // The directory where chats are stored const fileName = `${chatId}.json`; // Assuming the ID should be the filename @@ -378,7 +376,7 @@ export async function getChatById(chatId: string): Promise { return getChat(uri); } -export async function storeChat(chat: wasm.Chat): Promise { +export async function storeChat(chat: wasm.ChatWasm): Promise { try { const chatId = chat.sessionId; // Convert the chat instance to a JSON string @@ -404,13 +402,13 @@ export async function storeChat(chat: wasm.Chat): Promise { } } -export async function getLLMParams(): Promise { +export async function getLLMParams(): Promise { let modelVersion = await getOrSetModelVersion(); if (modelVersion === null) { - modelVersion = wasm.OpenAIModels.Gpt4; + modelVersion = wasm.GptModel.Gpt4; vscode.window.showErrorMessage( "Invalid model version, defaulting to Gpt4." ); } - return wasm.OpenAIParams.empty(modelVersion); + return wasm.ChatParamsWasm.empty(modelVersion); } diff --git a/webview/src/ components/chatContainer.tsx b/webview/src/ components/chatContainer.tsx index 0269ff3..3fae8d7 100644 --- a/webview/src/ components/chatContainer.tsx +++ b/webview/src/ components/chatContainer.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from 'react'; import ChatStream from './chatStream'; import { promptLLM, saveChat } from './vsceClient'; -import { Message } from '../../wasm/neatcoderInterface'; +import { MessageDataWasm as Message } from '../../wasm/neatcoderInterface'; import QuillEditor from './reactQuill'; import SendButton from './sendButton'; diff --git a/webview/src/ components/chatStream.tsx b/webview/src/ components/chatStream.tsx index 0d54126..5bc4caa 100644 --- a/webview/src/ components/chatStream.tsx +++ b/webview/src/ components/chatStream.tsx @@ -1,6 +1,6 @@ import React, { CSSProperties, useEffect, useRef, useState } from 'react'; import { LlmSvgIcon } from './llmAvatar'; -import { Message } from '../../wasm/neatcoderInterface'; +import { MessageDataWasm as Message } from '../../wasm/neatcoderInterface'; import hljs from './codeBlockStyle'; import ReactMarkdown from 'react-markdown'; diff --git a/webview/src/ components/vsceClient.tsx b/webview/src/ components/vsceClient.tsx index f474d9b..b8c8300 100644 --- a/webview/src/ components/vsceClient.tsx +++ b/webview/src/ components/vsceClient.tsx @@ -1,4 +1,4 @@ -import { Message } from "../../wasm/neatcoderInterface"; +import { MessageDataWasm as Message } from "../../wasm/neatcoderInterface"; import { vscode } from "../App"; export function promptLLM(msgs: Array, stream: boolean): ReadableStream { diff --git a/webview/wasm/neatcoderInterface.d.ts b/webview/wasm/neatcoderInterface.d.ts index e4df4b2..766c3ce 100644 --- a/webview/wasm/neatcoderInterface.d.ts +++ b/webview/wasm/neatcoderInterface.d.ts @@ -13,9 +13,12 @@ export interface AppData { taskPool: TaskPool; } -export interface Chat { - messages: Array; - models: Record; +export interface ChatParamsWasm { +} + +export interface ChatWasm { + messages: Array; + models: Record; sessionId: string; title: string; } @@ -32,6 +35,11 @@ export interface Database { schemas: Record; } +export interface GptMessageWasm { + content: string; + role: string; +} + export interface Interface { interface: any; interfaceType: number; @@ -50,31 +58,18 @@ export interface Language { language: number; } -export interface Message { - payload: OpenAIMsg; +export interface MessageDataWasm { + payload: GptMessageWasm; ts: Date; user: string; } -export interface Model { +export interface ModelDataWasm { id: string; interface: string; uri: string; } -export interface OpenAIMsg { - content: string; - role: string; -} - -export interface OpenAIParams { - max_tokens?: bigint; - model: number; - n?: bigint; - stream: boolean; - temperature?: number; -} - export interface Pipeline { order: Array; tasks: Record;