度過了昨天最難的部分,今天來繼續完善我們的專案吧!
*Ferris 3D by Ray March
🏮 今天完整的程式碼可以拉到最底下 Put it together 區塊或是在 GitHub 找到。
在昨天的 API 程式碼中,我們假設模型在伺服器 (Actix 後端) 開啟時就會被載入,並被放在 actix_web::web::Data wrapper 中,昨天的程式碼如下:
let model =
extract(|data: Data<Llama>, _connection: ConnectionInfo| async move { data.into_inner() })
.await
.unwrap();
所以我們今天就要來讓這個假設成立,在 [Day 12] - 鋼鐵草泥馬 🦙 LLM chatbot 🤖 (3/9)|Leptos 小教室 時提到後端的程式碼放在 src/main.rs 中,所以我們要先在這個檔案裡寫一個載入模型的函式。
這個函式請放在 async fn main() -> std::io::Result<()>
區塊之下,因為待會我們會在此函式區塊中使用它。
首先使用 cfg_if
巨集指定這個函式只在伺服器端編譯,並引入需要的函式庫與物件:
cfg_if! {
if #[cfg(feature = "ssr")] {
use std::env;
use actix_web::*;
use dotenv::dotenv;
use llm::models::Llama;
fn get_language_model() -> Llama {}
}
}
由於我們要用 .env 檔來設定模型的位址,所以要加入 dotenv crate,其方法一樣是在 Cargo.toml 的 [dependencies] 區塊最後面加上:
dotenv = { version = "0.15.0", optional = true}
並在 [features] 區塊 ssr 的 list 中也加上:
"dep:dotenv"
然後在專案的資料夾下建立一個 .env 檔,其中用 MODEL_PATH
來指定模型的位置,例如 (請換成實際的下載位置,要下載哪個模型請參考 [Day 15] - 鋼鐵草泥馬 🦙 LLM chatbot 🤖 (6/9)|GGML 量化 LLaMa):
MODEL_PATH="/home/ubuntu/iron-llama/Taiwan-LLaMa-13b-1.0.ggmlv3.q5_K_M.bin"
我們將函式命名為 get_language_model
,它會回傳 llm::models::Llama 結構體,也就是我們的模型,模型的載入參考 llm 官方文件,每一行的說明直接註解在上面:
fn get_language_model() -> Llama {
use std::path::PathBuf;
dotenv().ok();
let model_path = env::var("MODEL_PATH").expect("MODEL_PATH cannot be empty");
// load a GGML model from disk
llm::load::<Llama>(
// path to GGML file
&PathBuf::from(&model_path),
/// Read the vocabulary from the model if available, and use a simplistic tokenizer.
/// This is easy to use, but may not be the best choice for your use case, and is not
/// guaranteed to be available for all models.
llm::TokenizerSource::Embedded,
// llm::ModelParameters
Default::default(),
// load progress callback
llm::load_progress_callback_stdout
).unwrap_or_else(|err| panic!("Failed to load model: {err}"));
}
有了載入模型的函式後,我們可以在建立 HttpServer
前將模型包進前面提到的 Data
wrapper 中, 所以請在 async fn main() -> std::io::Result<()>
區塊中 let routes = generate_route_list(|| view! { <App/> });
之下加上:
let model = web::Data::new(get_language_model());
然後就能將模型加到 Actix 後端建立的程序之中,所以請在 HttpServer
區塊中 App::new()
之下加上 .app_data(model.clone())
:
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
**.app_data(model.clone())**
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
...
在底層這個 Data
wrapper 會把資料包進 Rust 的原子性參考計數 Arc<T> 中,其中字母 A 指的就是 Atomic,Arc
型別能讓數個 worker 能擁有接收端,它讓我們可以安全地在多重執行緒共享所有權。
會這麼做的原因是 Actix 後端的 HttpServer
預設上會開啟與系統實體 CPU 數量相同的 HTTP workers,而搭配 Rust 提供的 Arc
型別就能輕鬆地進行多執行緒處理,詳細請參考 Multi-Threading。
如果 cargo leptos watch
還在執行的話 (沒有請執行這行指令),可以看到終端機印出成功載入模型的資訊:
send
函式成功載入模型之後,converse
函式的假設就成立了,我們來填坑收尾吧!
首先把 src/app.rs 內的 todo!
巨集換成呼叫 converse(conversation()).await
(因為 converse
是異步函式,所以要 等 (await) 一下)。
其輸入值是 conversation
ReadSignal 呼叫 get()
後 (這裡用了函式呼叫式的簡寫) 取得的內容,也就是代表歷史對話的 Conversation
結構體。
最值得注意的是 converse
是實作在 src/api.rs 中的函式,其中包含的都是 伺服器端 的邏輯,但我們卻得以在 客戶端 呼叫它。
這一切都歸功於昨天提到的 #[server()]
巨集,會在客戶端自動生成指向 API 的 HTTP request,一切都被 Leptos 抽象化了!
今天總共修改了 3 個檔案,另外還建立了 1 個 .env 檔。
async fn favicon
之上的程式碼整理如下:
use cfg_if::cfg_if;
#[cfg(feature = "ssr")]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
use actix_files::Files;
use actix_web::*;
use iron_llama::app::*;
use leptos::*;
use leptos_actix::{generate_route_list, LeptosRoutes};
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|| view! { <App/> });
let model = web::Data::new(get_language_model());
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.app_data(model.clone())
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
// serve JS/WASM/CSS from `pkg`
.service(Files::new("/pkg", format!("{site_root}/pkg")))
// serve other assets from the `assets` directory
.service(Files::new("/assets", site_root))
// serve the favicon from /favicon.ico
.service(favicon)
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
|| view! { <App/> },
)
.app_data(web::Data::new(leptos_options.to_owned()))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
cfg_if! {
if #[cfg(feature = "ssr")] {
use std::env;
use actix_web::*;
use dotenv::dotenv;
use llm::models::Llama;
fn get_language_model() -> Llama {
use std::path::PathBuf;
dotenv().ok();
let model_path = env::var("MODEL_PATH").expect("MODEL_PATH cannot be empty");
// load a GGML model from disk
llm::load::<Llama>(
// path to GGML file
&PathBuf::from(&model_path),
/// Read the vocabulary from the model if available, and use a simplistic tokenizer.
/// This is easy to use, but may not be the best choice for your use case, and is not
/// guaranteed to be available for all models.
llm::TokenizerSource::Embedded,
// llm::ModelParameters
Default::default(),
// load progress callback
llm::load_progress_callback_stdout
).unwrap_or_else(|err| panic!("Failed to load model: {err}"))
}
}
}
converse
函式的先決條件,因此把 src.app.rs 的 async move { todo!(converse) }
改成了 async move { converse(conversation()).await }
好啦,今天就這樣囉,明天再來把前端的兩個主要區塊實作好就完成囉!