Spaces:
Sleeping
Sleeping
| 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?) | |
| } | |
| } | |
| 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()) | |
| } | |
| 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(¶ms.space_id).await?; | |
| let proposals = app.snapshot_client.proposals(¶ms.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(¶ms.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`. | |
| 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()) | |
| } | |
| } | |