batman
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
**/dist
|
2926
Cargo.lock
generated
Normal file
2926
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
Normal file
10
Cargo.toml
Normal 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
14
backend/Cargo.toml
Normal 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
4
backend/Dockerfile
Normal 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
11
backend/Rocket.toml
Normal 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
56
backend/src/main.rs
Normal 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
22
backend/src/models.rs
Normal 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
8
common/Cargo.toml
Normal 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
25
common/src/lib.rs
Normal 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
27
compose.yml
Normal 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
12
frontend/Cargo.toml
Normal 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
5
frontend/Dockerfile
Normal 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
11
frontend/build.rs
Normal 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
10
frontend/index.html
Normal 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
4
frontend/src/api/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
pub mod models;
|
||||
|
||||
pub static BASE: &'static str = env!("BASE");
|
18
frontend/src/api/models.rs
Normal file
18
frontend/src/api/models.rs
Normal 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),
|
||||
}
|
||||
}
|
59
frontend/src/components/chat.rs
Normal file
59
frontend/src/components/chat.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
}
|
53
frontend/src/components/mod.rs
Normal file
53
frontend/src/components/mod.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
}
|
57
frontend/src/components/models/list.rs
Normal file
57
frontend/src/components/models/list.rs
Normal 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>
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
2
frontend/src/components/models/mod.rs
Normal file
2
frontend/src/components/models/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod list;
|
||||
pub use list::List;
|
23
frontend/src/components/nav.rs
Normal file
23
frontend/src/components/nav.rs
Normal 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
35
frontend/src/lib.rs
Normal 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
26
frontend/src/main.rs
Normal 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
12
frontend/styles.css
Normal 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
41
nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user