rig-boxone-demo / src /main.rs
stopher's picture
misc: cleanup
4d39595 unverified
pub mod snapshot;
use std::sync::Arc;
use axum::{
extract::{Query, State},
response::{Html, IntoResponse},
routing::get,
Router,
};
use futures::{stream, StreamExt};
use reqwest::StatusCode;
use rig::{agent::Agent, completion::Prompt, providers::openai};
use snapshot::{proposals, space};
struct AppState {
pub agent: Agent<openai::CompletionModel>,
// pub alchemy: alchemy::AlchemyApiClient,
// pub debank: debank::DebankApiClient,
pub snapshot_client: snapshot::SnapshotClient,
}
impl AppState {
pub async fn summarize(
&self,
space: &Option<space::SpaceSpace>,
proposal: &proposals::ProposalsProposals,
) -> anyhow::Result<String> {
let prompt = format!(
include_str!("../resources/prompt_summarize_snapshot.md"),
DAO_METADATA = serde_yaml::to_string(space)?,
PROPOSAL_ID = proposal.id,
PROPOSAL_TITLE = proposal.title,
PROPOSAL_TEXT = proposal.body.clone().unwrap_or("NO TEXT".to_string()),
PROPOSAL_CHOICES = serde_yaml::to_string(&proposal.choices)?,
);
Ok(self.agent.prompt(&prompt).await?)
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// required to enable CloudWatch error logging by the runtime
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
// disable printing the name of the module in every log line.
.with_target(false)
// this needs to be set to false, otherwise ANSI color codes will
// show up in a confusing manner in CloudWatch logs.
.with_ansi(false)
.init();
let openai = openai::Client::from_env();
let app = Router::new()
.route("/", get(root))
// .route("/explain-tx", get(explain_tx))
.route("/summarize", get(summarize))
.with_state(Arc::new(AppState {
agent: openai
.agent(openai::GPT_4O)
.preamble(include_str!("../resources/preamble_summarize_snapshot.md"))
.build(),
// alchemy: alchemy::AlchemyApiClient::from_env(),
// debank: debank::DebankApiClient::from_env(),
snapshot_client: snapshot::SnapshotClient::new(),
}));
// run it
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
async fn root() -> Html<String> {
Html(include_str!("../resources/index.html").to_string())
}
#[derive(Debug, serde::Deserialize)]
struct SummarizeParams {
pub space_id: String,
}
async fn summarize(
State(app): State<Arc<AppState>>,
Query(params): Query<SummarizeParams>,
) -> Result<Html<String>, AppError> {
tracing::info!("Snapshot space id: {}", params.space_id);
let space = app.snapshot_client.space(&params.space_id).await?;
let proposals = app.snapshot_client.proposals(&params.space_id).await?;
let summaries = stream::iter(proposals)
.map(|proposal| {
tracing::info!("Summarizing proposal {}", proposal.title);
let app_ref = &app;
let space_ref = &space;
async move {
match app_ref.summarize(space_ref, &proposal).await {
Ok(summary) => summary,
Err(err) => {
tracing::error!("Error summarizing proposal {}: {:?}", proposal.title, err);
"".to_string()
}
}
}
})
.buffer_unordered(10)
.collect::<Vec<_>>()
.await
.into_iter()
.filter(|summary| !summary.is_empty())
.collect::<Vec<_>>();
if summaries.is_empty() {
Ok(Html("<h2>No active proposals</h2>".to_string()))
} else {
Ok(Html(format!(
r#"
<div id="summaries">{}</div>
"#,
summaries.join("\n")
)))
}
}
// #[derive(Debug, serde::Deserialize)]
// struct ExplainTxParams {
// pub txhash: String,
// }
// async fn explain_tx(
// State(app): State<Arc<AppState>>,
// Query(params): Query<ExplainTxParams>,
// ) -> Html<String> {
// println!("Tx hash: {}", params.txhash);
// let tx_data = app
// .alchemy
// .eth_get_transaction_by_hash(&params.txhash)
// .await
// .unwrap();
// let actions = app.debank.explain_tx(tx_data.result.into()).await.unwrap();
// println!("{}", serde_json::to_string_pretty(&actions).unwrap());
// let response = app
// .agent
// .prompt(&serde_json::to_string_pretty(&actions).unwrap())
// .await
// .unwrap();
// Html(format!(
// r#"
// <div>
// <code>
// {}
// </code>
// </div>
// "#,
// response
// ))
// }
// ================================================================
// Error handling stuff
// ================================================================
// Make our own error that wraps `anyhow::Error`.
#[derive(Debug)]
pub struct AppError(anyhow::Error);
// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {:?}", self.0),
)
.into_response()
}
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}