- Notifications
You must be signed in to change notification settings - Fork328
Dan rag page components#1543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Merged
Uh oh!
There was an error while loading.Please reload this page.
Merged
Changes fromall commits
Commits
Show all changes
12 commits Select commitHold shift + click to select a range
467d4a8
checkpoint
chillenberger5174e72
add create table to demo.js
chillenberger6e7d96d
lint
chillenberger9e2cda0
remove dead js, add rag to marketing navbar
chillenberger20056c7
Merge branch 'master' into dan-rag-page-components
chillenberger3371a96
add space
chillenberger34161ed
remove dead js, fmt
chillenbergerb3b9107
move editor and demo.js to dashboard
chillenberger6da4d43
add turbo frame component, add example of delivering editor by turbof…
chillenberger574c05d
hide rag page for now
chillenberger3e2bfa1
Merge branch 'master' into dan-rag-page-components
chillenbergercf753f7
fmt
chillenbergerFile filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
12 changes: 11 additions & 1 deletionpgml-dashboard/Cargo.lock
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
1 change: 1 addition & 0 deletionspgml-dashboard/Cargo.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
285 changes: 285 additions & 0 deletionspgml-dashboard/src/api/code_editor.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,285 @@ | ||
use crate::components::code_editor::Editor; | ||
use crate::components::turbo::TurboFrame; | ||
use anyhow::Context; | ||
use once_cell::sync::OnceCell; | ||
use sailfish::TemplateOnce; | ||
use serde::Serialize; | ||
use sqlparser::dialect::PostgreSqlDialect; | ||
use sqlx::{postgres::PgPoolOptions, Executor, PgPool, Row}; | ||
use crate::responses::ResponseOk; | ||
use rocket::route::Route; | ||
static READONLY_POOL: OnceCell<PgPool> = OnceCell::new(); | ||
static ERROR: &str = | ||
"Thanks for trying PostgresML! If you would like to run more queries, sign up for an account and create a database."; | ||
fn get_readonly_pool() -> PgPool { | ||
READONLY_POOL | ||
.get_or_init(|| { | ||
PgPoolOptions::new() | ||
.max_connections(1) | ||
.idle_timeout(std::time::Duration::from_millis(60_000)) | ||
.max_lifetime(std::time::Duration::from_millis(60_000)) | ||
.connect_lazy(&std::env::var("CHATBOT_DATABASE_URL").expect("CHATBOT_DATABASE_URL not set")) | ||
.expect("could not build lazy database connection") | ||
}) | ||
.clone() | ||
} | ||
fn check_query(query: &str) -> anyhow::Result<()> { | ||
let ast = sqlparser::parser::Parser::parse_sql(&PostgreSqlDialect {}, query)?; | ||
if ast.len() != 1 { | ||
anyhow::bail!(ERROR); | ||
} | ||
let query = ast | ||
.into_iter() | ||
.next() | ||
.with_context(|| "impossible, ast is empty, even though we checked")?; | ||
match query { | ||
sqlparser::ast::Statement::Query(query) => match *query.body { | ||
sqlparser::ast::SetExpr::Select(_) => (), | ||
_ => anyhow::bail!(ERROR), | ||
}, | ||
_ => anyhow::bail!(ERROR), | ||
}; | ||
Ok(()) | ||
} | ||
#[derive(FromForm, Debug)] | ||
pub struct PlayForm { | ||
pub query: String, | ||
} | ||
pub async fn play(sql: &str) -> anyhow::Result<String> { | ||
check_query(sql)?; | ||
let pool = get_readonly_pool(); | ||
let row = sqlx::query(sql).fetch_one(&pool).await?; | ||
let transform: serde_json::Value = row.try_get(0)?; | ||
Ok(serde_json::to_string_pretty(&transform)?) | ||
} | ||
/// Response expected by the frontend. | ||
#[derive(Serialize)] | ||
struct StreamResponse { | ||
error: Option<String>, | ||
result: Option<String>, | ||
} | ||
impl StreamResponse { | ||
fn from_error(error: &str) -> Self { | ||
StreamResponse { | ||
error: Some(error.to_string()), | ||
result: None, | ||
} | ||
} | ||
fn from_result(result: &str) -> Self { | ||
StreamResponse { | ||
error: None, | ||
result: Some(result.to_string()), | ||
} | ||
} | ||
} | ||
impl ToString for StreamResponse { | ||
fn to_string(&self) -> String { | ||
serde_json::to_string(self).unwrap() | ||
} | ||
} | ||
/// An async iterator over a PostgreSQL cursor. | ||
#[derive(Debug)] | ||
struct AsyncResult<'a> { | ||
/// Open transaction. | ||
transaction: sqlx::Transaction<'a, sqlx::Postgres>, | ||
cursor_name: String, | ||
} | ||
impl<'a> AsyncResult<'a> { | ||
async fn from_message(message: ws::Message) -> anyhow::Result<Self> { | ||
if let ws::Message::Text(query) = message { | ||
let request = serde_json::from_str::<serde_json::Value>(&query)?; | ||
let query = request["sql"] | ||
.as_str() | ||
.context("Error sql key is required in websocket")?; | ||
Self::new(&query).await | ||
} else { | ||
anyhow::bail!(ERROR) | ||
} | ||
} | ||
/// Create new AsyncResult given a query. | ||
async fn new(query: &str) -> anyhow::Result<Self> { | ||
let cursor_name = format!(r#""{}""#, crate::utils::random_string(12)); | ||
// Make sure it's a SELECT. Can't do too much damage there. | ||
check_query(query)?; | ||
let pool = get_readonly_pool(); | ||
let mut transaction = pool.begin().await?; | ||
let query = format!("DECLARE {} CURSOR FOR {}", cursor_name, query); | ||
info!( | ||
"[stream] query: {}", | ||
query.trim().split("\n").collect::<Vec<&str>>().join(" ") | ||
); | ||
match transaction.execute(query.as_str()).await { | ||
Ok(_) => (), | ||
Err(err) => { | ||
info!("[stream] query error: {:?}", err); | ||
anyhow::bail!(err); | ||
} | ||
} | ||
Ok(AsyncResult { | ||
transaction, | ||
cursor_name, | ||
}) | ||
} | ||
/// Fetch a row from the cursor, get the first column, | ||
/// decode the value and return it as a String. | ||
async fn next(&mut self) -> anyhow::Result<Option<String>> { | ||
use serde_json::Value; | ||
let result = sqlx::query(format!("FETCH 1 FROM {}", self.cursor_name).as_str()) | ||
.fetch_optional(&mut *self.transaction) | ||
.await?; | ||
if let Some(row) = result { | ||
let _column = row.columns().get(0).with_context(|| "no columns")?; | ||
// Handle pgml.embed() which returns an array of floating points. | ||
if let Ok(value) = row.try_get::<Vec<f32>, _>(0) { | ||
return Ok(Some(serde_json::to_string(&value)?)); | ||
} | ||
// Anything that just returns a String, e.g. pgml.version(). | ||
if let Ok(value) = row.try_get::<String, _>(0) { | ||
return Ok(Some(value)); | ||
} | ||
// Array of strings. | ||
if let Ok(value) = row.try_get::<Vec<String>, _>(0) { | ||
return Ok(Some(value.join(""))); | ||
} | ||
// Integers. | ||
if let Ok(value) = row.try_get::<i64, _>(0) { | ||
return Ok(Some(value.to_string())); | ||
} | ||
if let Ok(value) = row.try_get::<i32, _>(0) { | ||
return Ok(Some(value.to_string())); | ||
} | ||
if let Ok(value) = row.try_get::<f64, _>(0) { | ||
return Ok(Some(value.to_string())); | ||
} | ||
if let Ok(value) = row.try_get::<f32, _>(0) { | ||
return Ok(Some(value.to_string())); | ||
} | ||
// Handle functions that return JSONB, | ||
// e.g. pgml.transform() | ||
if let Ok(value) = row.try_get::<Value, _>(0) { | ||
return Ok(Some(match value { | ||
Value::Array(ref values) => { | ||
let first_value = values.first(); | ||
match first_value { | ||
Some(Value::Object(_)) => serde_json::to_string(&value)?, | ||
_ => values | ||
.into_iter() | ||
.map(|v| v.as_str().unwrap_or("").to_string()) | ||
.collect::<Vec<String>>() | ||
.join(""), | ||
} | ||
} | ||
value => serde_json::to_string(&value)?, | ||
})); | ||
} | ||
} | ||
Ok(None) | ||
} | ||
async fn close(mut self) -> anyhow::Result<()> { | ||
self.transaction | ||
.execute(format!("CLOSE {}", self.cursor_name).as_str()) | ||
.await?; | ||
self.transaction.rollback().await?; | ||
Ok(()) | ||
} | ||
} | ||
#[get("/code_editor/play/stream")] | ||
pub async fn play_stream(ws: ws::WebSocket) -> ws::Stream!['static] { | ||
ws::Stream! { ws => | ||
for await message in ws { | ||
let message = match message { | ||
Ok(message) => message, | ||
Err(_err) => continue, | ||
}; | ||
let mut got_something = false; | ||
match AsyncResult::from_message(message).await { | ||
Ok(mut result) => { | ||
loop { | ||
match result.next().await { | ||
Ok(Some(result)) => { | ||
got_something = true; | ||
yield ws::Message::from(StreamResponse::from_result(&result).to_string()); | ||
} | ||
Err(err) => { | ||
yield ws::Message::from(StreamResponse::from_error(&err.to_string()).to_string()); | ||
break; | ||
} | ||
Ok(None) => { | ||
if !got_something { | ||
yield ws::Message::from(StreamResponse::from_error(ERROR).to_string()); | ||
} | ||
break; | ||
} | ||
} | ||
}; | ||
match result.close().await { | ||
Ok(_) => (), | ||
Err(err) => { | ||
info!("[stream] error closing: {:?}", err); | ||
} | ||
}; | ||
} | ||
Err(err) => { | ||
yield ws::Message::from(StreamResponse::from_error(&err.to_string()).to_string()); | ||
} | ||
} | ||
}; | ||
} | ||
} | ||
#[get("/code_editor/embed?<id>")] | ||
pub fn embed_editor(id: String) -> ResponseOk { | ||
let comp = Editor::new(); | ||
let rsp = TurboFrame::new().set_target_id(&id).set_content(comp.into()); | ||
return ResponseOk(rsp.render_once().unwrap()); | ||
} | ||
pub fn routes() -> Vec<Route> { | ||
routes![play_stream, embed_editor,] | ||
} |
2 changes: 2 additions & 0 deletionspgml-dashboard/src/api/mod.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletionspgml-dashboard/src/components/cards/mod.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.