This commit is contained in:
2024-07-17 17:54:48 +02:00
commit 4991467d32
26 changed files with 3473 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
**/dist

2926
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

10
Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[workspace]
members = ["frontend", "backend"]
resolver = "2"
[workspace.dependencies]
allpaca-models = { path = "common" }
reqwest = { version = "0.12.5", features = ["blocking"] }
tokio = "1.38.0"
prost = "0.13.1"
json = "0.12.4"

14
backend/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "oracle"
version = "0.1.0"
edition = "2021"
[dependencies]
rocket = { version = "0.5.1", features = ["serde_json"] }
ollama-rs = { version = "0.2.0", features = ["stream"] }
allpaca-models = { workspace = true }
reqwest = { workspace = true }
tokio = { workspace = true }
prost = { workspace = true }
json = { workspace = true }
lazy_static = "1.5.0"

4
backend/Dockerfile Normal file
View File

@@ -0,0 +1,4 @@
FROM rust:slim
RUN apt update && apt install -y pkg-config librust-openssl-dev && apt clean
RUN cargo install cargo-watch
CMD [ "cargo", "watch", "-x", "run" ]

11
backend/Rocket.toml Normal file
View File

@@ -0,0 +1,11 @@
[default]
address = "0.0.0.0"
port = 80
ip_header = "X-Real-IP"
log_level = "normal"
temp_dir = "/tmp"
workers = 4
[debug]
log_level = "debug"

56
backend/src/main.rs Normal file
View File

@@ -0,0 +1,56 @@
#[macro_use] extern crate rocket;
mod models;
use models::list;
use lazy_static::lazy_static;
use tokio::sync::Mutex;
use std::sync::Arc;
use ollama_rs::Ollama;
use rocket::{
http::{Header, Status},
response::Responder,
Request};
lazy_static! {
pub static ref OLLAMA: Arc<Mutex<Ollama>> = Arc::new(Mutex::new(Ollama::new("http://ollama", 11434)));
}
pub static BASE: &'static str = "http://ollama:11434/api";
#[macro_export]
macro_rules! response {
($name:ident, $body:ty) => {
pub struct $name {
pub status: rocket::http::Status,
pub body: $body,
}
#[rocket::async_trait]
impl<'r> Responder<'r, 'static> for $name {
fn respond_to(self, _request: &'r Request) -> rocket::response::Result<'static> {
let body: Vec<u8> = self.body.into();
rocket::response::Response::build()
.header(Header::new("Content-Type", "application/x-protobuf"))
.sized_body(body.len(), std::io::Cursor::new(body))
.status(self.status)
.ok()
}
}
};
}
response!(Response, &'static str);
#[get("/")]
async fn index() -> Response {
Response {
status: Status::ImATeapot,
body: "Hello, world!",
}
}
#[rocket::main]
async fn main() -> Result<(), rocket::Error> {
rocket::build()
.mount("/", routes![index, models::list])
.launch().await?;
Ok(())
}

22
backend/src/models.rs Normal file
View File

@@ -0,0 +1,22 @@
use rocket::{http::{Header, Status}, response::Responder, Request, Response};
use json::JsonValue;
use allpaca_models::{Models, Model};
use super::{OLLAMA, BASE, response};
response!(ListResponse, Models);
#[get("/models/list")]
pub async fn list() -> ListResponse {
let models = OLLAMA.lock().await.list_local_models().await.unwrap();
ListResponse {
status: Status::Ok,
body: Models(models.iter().map(|model| {
Model {
modified_at: model.modified_at.clone(),
size: model.size.to_string(),
name: model.name.clone(),
}
}).collect()),
}
}

8
common/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "allpaca-models"
version = "0.1.0"
edition = "2021"
[dependencies]
prost = { workspace = true }
json = { workspace = true }

25
common/src/lib.rs Normal file
View File

@@ -0,0 +1,25 @@
use json::JsonValue;
use prost::{
alloc::{
string::String,
vec::Vec,
}, *};
#[derive(Clone, PartialEq, Message)]
pub struct Models(#[prost(message, repeated)] pub Vec<Model>);
#[derive(Clone, PartialEq, Message)]
pub struct Model {
#[prost(string)]
pub modified_at: String,
#[prost(string)]
pub size: String,
#[prost(string)]
pub name: String,
}
impl Into<Vec<u8>> for Models {
fn into(self) -> Vec<u8> {
self.encode_to_vec()
}
}

27
compose.yml Normal file
View File

@@ -0,0 +1,27 @@
services:
ollama:
image: ollama/ollama:latest
volumes:
- /root/.ollama:/root/.ollama
backend:
build: backend
working_dir: /opt/allpaca/backend
volumes:
- ./:/opt/allpaca
environment:
- RUST_BACKTRACE=1
frontend:
build: frontend
working_dir: /opt/allpaca/frontend
volumes:
- ./:/opt/allpaca
nginx:
image: nginx:latest
volumes:
- ./frontend/dist:/srv/www
- ./nginx.conf:/etc/nginx/nginx.conf
ports:
- 80:80

12
frontend/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "allpaca"
version = "0.1.0"
edition = "2021"
[dependencies]
allpaca-models = { workspace = true }
reqwest = { workspace = true }
prost = { workspace = true }
yew = { version = "0.21.0", features = ["csr"] }
wasm-bindgen = "0.2.92"
gloo-net = "0.5.0"

5
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM rust:slim
RUN rustup default nightly
RUN rustup target add wasm32-unknown-unknown
RUN cargo install trunk
CMD [ "trunk", "watch" ]

11
frontend/build.rs Normal file
View File

@@ -0,0 +1,11 @@
#[cfg(not(debug_assertions))]
pub static BASE: &'static str = "https://gpt.42069.no/api";
#[cfg(debug_assertions)]
pub static BASE: &'static str = "http://localhost/api";
fn main() {
if cfg!(debug_assertions) {
println!("cargo::rustc-env=BASE={}", BASE);
}
}

10
frontend/index.html Normal file
View File

@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Allpaca</title>
<link data-trunk rel="rust" />
<link data-trunk rel="css" href="./styles.css" />
</head>
<body></body>
</html>

4
frontend/src/api/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod models;
pub static BASE: &'static str = env!("BASE");

View File

@@ -0,0 +1,18 @@
use prost::Message;
use crate::Models;
macro_rules! path {
($path:expr) => {
super::BASE.to_owned() + "/models" + $path
};
}
pub async fn list() -> Result<Models, String> {
match Models::decode(
reqwest::get(path!("/list"))
.await.unwrap().text().await.unwrap().as_bytes()
) {
Err(_err) => Err("API response was not valid protobuf format".to_owned()),
Ok(models) => Ok(models),
}
}

View File

@@ -0,0 +1,59 @@
use yew::prelude::*;
pub struct Message {
pub content: String,
pub is_user: bool,
}
pub enum ChatMsg {
Send
}
pub struct Chat {
pub history: Vec<Message>,
}
impl Component for Chat {
type Properties = ();
type Message = ChatMsg;
fn create(_ctx: &Context<Self>) -> Self {
Self { history: vec![
Message {
content: "How may I help you today?".to_owned(),
is_user: false,
}]
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
ChatMsg::Send => false,
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div>
<div class="chat">
{
self.history.iter().map(|message| {
html! {
<div class={ if message.is_user { "user" } else { "assistant" } }>
<p>{ &message.content }</p>
</div>
}
}).collect::<Html>()
}
</div>
<form class="chat">
<div>
<input type="textfield" required=true />
</div>
<div>
<button onclick={
ctx.link().callback(|_| ChatMsg::Send)
}>{ "Send" }</button>
</div>
</form>
</div>
}
}
}

View File

@@ -0,0 +1,53 @@
use yew::prelude::*;
pub mod models;
pub mod chat;
pub mod nav;
pub use chat::Chat;
pub enum FetchMsg {
Response(String), Fetch,
}
pub struct Fetcher {
res: String,
}
impl From<String> for FetchMsg {
fn from(value: String) -> Self {
Self::Response(value)
}
}
impl Component for Fetcher {
type Properties = ();
type Message = FetchMsg;
fn create(_ctx: &Context<Self>) -> Self {
Self { res: String::new() }
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
FetchMsg::Response(res) => {
self.res = res;
true
},
FetchMsg::Fetch => {
ctx.link().send_future((async move || {
FetchMsg::from(reqwest::get("http://localhost/api").await.unwrap().text().await.unwrap())
})());
false
},
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div>
<button onclick={
ctx.link().callback(|_| FetchMsg::Fetch)
}>{ "balls" }</button>
<p>{ &self.res }</p>
</div>
}
}
}

View File

@@ -0,0 +1,57 @@
use yew::{Component, Html, html};
use crate::{STORE, ModelsResult, api};
pub struct List(pub Option<ModelsResult>);
pub enum ListMsg {
List(ModelsResult)
}
impl Component for List {
type Properties = ();
type Message = ListMsg;
fn create(ctx: &yew::Context<Self>) -> Self {
let models = STORE.models.lock().unwrap();
if models.is_none() {
ctx.link().send_future((async move || {
ListMsg::List(api::models::list().await)
})());
Self(None)
} else {
Self(models.clone())
}
}
fn update(&mut self, _ctx: &yew::Context<Self>, msg: Self::Message) -> bool {
match msg {
ListMsg::List(models) => self.0 = Some(models),
}
true
}
fn view(&self, _ctx: &yew::Context<Self>) -> Html {
match &self.0 {
None => html! { <div>{ "Loading..." }</div> },
Some(result) => {
match result {
Err(err) => html! { <div class="error">{ err }</div> },
Ok(models) => html! {
<ul class="model-list">
{
models.0.iter().map(|model| {
let size = model.size.parse::<usize>().unwrap();
html! {
<li>
<ul>
<li>{ "Name: "} { &model.name }</li>
<li>{ "Size: "} { size / ( 1024 * 1024) } { "MiB" }</li>
<li>{ "Modified at: "} { &model.modified_at }</li>
</ul>
</li>
}
}).collect::<Html>()
}
</ul>
},
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
mod list;
pub use list::List;

View File

@@ -0,0 +1,23 @@
use yew::prelude::*;
use allpaca_models::Model;
#[derive(Clone, PartialEq, Properties)]
pub struct Props {
// models: Vec<Model>,
}
#[function_component]
pub fn Bar(props: &Props) -> Html {
html! {
<ul>
<li class="model-dropdown">
<select>
// { props.models.iter().map(|model| {
// html! { <option>{model.name.clone()}</option> }
// }).collect::<Html>() }
</select>
</li>
</ul>
}
}

35
frontend/src/lib.rs Normal file
View File

@@ -0,0 +1,35 @@
#![feature(const_trait_impl)]
#![feature(async_closure)]
use std::sync::Mutex;
use yew::Callback;
pub mod components;
pub mod api;
use allpaca_models::Models;
pub type ModelsResult = Result<Models, String>;
pub struct Store {
pub models: Mutex<Option<ModelsResult>>,
}
pub static STORE: Store = Store {
models: Mutex::new(None),
};
impl Store {
fn set_models(&mut self, models: ModelsResult) {
*self.models.lock().unwrap() = Some(models);
}
}
use wasm_bindgen::prelude::wasm_bindgen;
#[cfg(debug_assertions)]
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
pub fn log(s: &str);
}

26
frontend/src/main.rs Normal file
View File

@@ -0,0 +1,26 @@
use yew::prelude::*;
use allpaca::components::*;
#[cfg(debug_assertions)]
use allpaca::log;
#[function_component]
fn App() -> Html {
html! {
<>
<div class="navbar">
<nav::Bar></nav::Bar>
</div>
<models::List></models::List>
<Chat></Chat>
<div>
<Fetcher></Fetcher>
</div>
</>
}
}
fn main() {
yew::Renderer::<App>::new().render();
}

12
frontend/styles.css Normal file
View File

@@ -0,0 +1,12 @@
body {
background-color: #333;
color: lime;
}
.navbar, .model-list {
list-style: none;
}
.model-list > li {
margin: 1vh;
}

41
nginx.conf Normal file
View File

@@ -0,0 +1,41 @@
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
worker_processes 2;
user nginx;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
location / {
index index.html;
root /srv/www;
}
location /api {
rewrite /api/?(.*) /$1 break;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://backend:80;
}
}
}