notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

lib.rs (13838B)


      1 use async_openai::{
      2     config::OpenAIConfig,
      3     types::{ChatCompletionRequestMessage, CreateChatCompletionRequest},
      4     Client,
      5 };
      6 use chrono::{Duration, Local};
      7 use egui_wgpu::RenderState;
      8 use enostr::KeypairUnowned;
      9 use futures::StreamExt;
     10 use nostrdb::Transaction;
     11 use notedeck::{AppAction, AppContext, AppResponse};
     12 use std::collections::HashMap;
     13 use std::string::ToString;
     14 use std::sync::mpsc::{self, Receiver};
     15 use std::sync::Arc;
     16 
     17 pub use avatar::DaveAvatar;
     18 pub use config::ModelConfig;
     19 pub use messages::{DaveApiResponse, Message};
     20 pub use quaternion::Quaternion;
     21 pub use tools::{
     22     PartialToolCall, QueryCall, QueryResponse, Tool, ToolCall, ToolCalls, ToolResponse,
     23     ToolResponses,
     24 };
     25 pub use ui::{DaveAction, DaveResponse, DaveUi};
     26 pub use vec3::Vec3;
     27 
     28 mod avatar;
     29 mod config;
     30 pub(crate) mod mesh;
     31 mod messages;
     32 mod quaternion;
     33 mod tools;
     34 mod ui;
     35 mod vec3;
     36 
     37 pub struct Dave {
     38     chat: Vec<Message>,
     39     /// A 3d representation of dave.
     40     avatar: Option<DaveAvatar>,
     41     input: String,
     42     tools: Arc<HashMap<String, Tool>>,
     43     client: async_openai::Client<OpenAIConfig>,
     44     incoming_tokens: Option<Receiver<DaveApiResponse>>,
     45     model_config: ModelConfig,
     46 }
     47 
     48 /// Calculate an anonymous user_id from a keypair
     49 fn calculate_user_id(keypair: KeypairUnowned) -> String {
     50     use sha2::{Digest, Sha256};
     51     // pubkeys have degraded privacy, don't do that
     52     let key_input = keypair
     53         .secret_key
     54         .map(|sk| sk.as_secret_bytes())
     55         .unwrap_or(keypair.pubkey.bytes());
     56     let hex_key = hex::encode(key_input);
     57     let input = format!("{hex_key}notedeck_dave_user_id");
     58     hex::encode(Sha256::digest(input))
     59 }
     60 
     61 impl Dave {
     62     pub fn avatar_mut(&mut self) -> Option<&mut DaveAvatar> {
     63         self.avatar.as_mut()
     64     }
     65 
     66     fn system_prompt() -> Message {
     67         let now = Local::now();
     68         let yesterday = now - Duration::hours(24);
     69         let date = now.format("%Y-%m-%d %H:%M:%S");
     70         let timestamp = now.timestamp();
     71         let yesterday_timestamp = yesterday.timestamp();
     72 
     73         Message::System(format!(
     74             r#"
     75 You are an AI agent for the nostr protocol called Dave, created by Damus. nostr is a decentralized social media and internet communications protocol. You are embedded in a nostr browser called 'Damus Notedeck'.
     76 
     77 - The current date is {date} ({timestamp} unix timestamp if needed for queries).
     78 
     79 - Yesterday (-24hrs) was {yesterday_timestamp}. You can use this in combination with `since` queries for pulling notes for summarizing notes the user might have missed while they were away.
     80 
     81 # Response Guidelines
     82 
     83 - You *MUST* call the present_notes tool with a list of comma-separated note id references when referring to notes so that the UI can display them. Do *NOT* include note id references in the text response, but you *SHOULD* use ^1, ^2, etc to reference note indices passed to present_notes.
     84 - When a user asks for a digest instead of specific query terms, make sure to include both since and until to pull notes for the correct range.
     85 - When tasked with open-ended queries such as looking for interesting notes or summarizing the day, make sure to add enough notes to the context (limit: 100-200) so that it returns enough data for summarization.
     86 "#
     87         ))
     88     }
     89 
     90     pub fn new(render_state: Option<&RenderState>) -> Self {
     91         let model_config = ModelConfig::default();
     92         //let model_config = ModelConfig::ollama();
     93         let client = Client::with_config(model_config.to_api());
     94 
     95         let input = "".to_string();
     96         let avatar = render_state.map(DaveAvatar::new);
     97         let mut tools: HashMap<String, Tool> = HashMap::new();
     98         for tool in tools::dave_tools() {
     99             tools.insert(tool.name().to_string(), tool);
    100         }
    101 
    102         Dave {
    103             client,
    104             avatar,
    105             incoming_tokens: None,
    106             tools: Arc::new(tools),
    107             input,
    108             model_config,
    109             chat: vec![],
    110         }
    111     }
    112 
    113     /// Process incoming tokens from the ai backend
    114     fn process_events(&mut self, app_ctx: &AppContext) -> bool {
    115         // Should we continue sending requests? Set this to true if
    116         // we have tool responses to send back to the ai
    117         let mut should_send = false;
    118 
    119         let Some(recvr) = &self.incoming_tokens else {
    120             return should_send;
    121         };
    122 
    123         while let Ok(res) = recvr.try_recv() {
    124             if let Some(avatar) = &mut self.avatar {
    125                 avatar.random_nudge();
    126             }
    127             match res {
    128                 DaveApiResponse::Failed(err) => self.chat.push(Message::Error(err)),
    129 
    130                 DaveApiResponse::Token(token) => match self.chat.last_mut() {
    131                     Some(Message::Assistant(msg)) => *msg = msg.clone() + &token,
    132                     Some(_) => self.chat.push(Message::Assistant(token)),
    133                     None => {}
    134                 },
    135 
    136                 DaveApiResponse::ToolCalls(toolcalls) => {
    137                     tracing::info!("got tool calls: {:?}", toolcalls);
    138                     self.chat.push(Message::ToolCalls(toolcalls.clone()));
    139 
    140                     let txn = Transaction::new(app_ctx.ndb).unwrap();
    141                     for call in &toolcalls {
    142                         // execute toolcall
    143                         match call.calls() {
    144                             ToolCalls::PresentNotes(present) => {
    145                                 self.chat.push(Message::ToolResponse(ToolResponse::new(
    146                                     call.id().to_owned(),
    147                                     ToolResponses::PresentNotes(present.note_ids.len() as i32),
    148                                 )));
    149 
    150                                 should_send = true;
    151                             }
    152 
    153                             ToolCalls::Invalid(invalid) => {
    154                                 should_send = true;
    155 
    156                                 self.chat.push(Message::tool_error(
    157                                     call.id().to_string(),
    158                                     invalid.error.clone(),
    159                                 ));
    160                             }
    161 
    162                             ToolCalls::Query(search_call) => {
    163                                 should_send = true;
    164 
    165                                 let resp = search_call.execute(&txn, app_ctx.ndb);
    166                                 self.chat.push(Message::ToolResponse(ToolResponse::new(
    167                                     call.id().to_owned(),
    168                                     ToolResponses::Query(resp),
    169                                 )))
    170                             }
    171                         }
    172                     }
    173                 }
    174             }
    175         }
    176 
    177         should_send
    178     }
    179 
    180     fn ui(&mut self, app_ctx: &mut AppContext, ui: &mut egui::Ui) -> DaveResponse {
    181         /*
    182         let rect = ui.available_rect_before_wrap();
    183         if let Some(av) = self.avatar.as_mut() {
    184             av.render(rect, ui);
    185             ui.ctx().request_repaint();
    186         }
    187         DaveResponse::default()
    188             */
    189 
    190         DaveUi::new(self.model_config.trial, &self.chat, &mut self.input).ui(app_ctx, ui)
    191     }
    192 
    193     fn handle_new_chat(&mut self) {
    194         self.chat = vec![];
    195         self.input.clear();
    196     }
    197 
    198     /// Handle a user send action triggered by the ui
    199     fn handle_user_send(&mut self, app_ctx: &AppContext, ui: &egui::Ui) {
    200         self.chat.push(Message::User(self.input.clone()));
    201         self.send_user_message(app_ctx, ui.ctx());
    202         self.input.clear();
    203     }
    204 
    205     fn send_user_message(&mut self, app_ctx: &AppContext, ctx: &egui::Context) {
    206         let messages: Vec<ChatCompletionRequestMessage> = {
    207             let txn = Transaction::new(app_ctx.ndb).expect("txn");
    208             self.chat
    209                 .iter()
    210                 .filter_map(|c| c.to_api_msg(&txn, app_ctx.ndb))
    211                 .collect()
    212         };
    213         tracing::debug!("sending messages, latest: {:?}", messages.last().unwrap());
    214 
    215         let user_id = calculate_user_id(app_ctx.accounts.get_selected_account().keypair());
    216 
    217         let ctx = ctx.clone();
    218         let client = self.client.clone();
    219         let tools = self.tools.clone();
    220         let model_name = self.model_config.model().to_owned();
    221 
    222         let (tx, rx) = mpsc::channel();
    223         self.incoming_tokens = Some(rx);
    224 
    225         tokio::spawn(async move {
    226             let mut token_stream = match client
    227                 .chat()
    228                 .create_stream(CreateChatCompletionRequest {
    229                     model: model_name,
    230                     stream: Some(true),
    231                     messages,
    232                     tools: Some(tools::dave_tools().iter().map(|t| t.to_api()).collect()),
    233                     user: Some(user_id),
    234                     ..Default::default()
    235                 })
    236                 .await
    237             {
    238                 Err(err) => {
    239                     tracing::error!("openai chat error: {err}");
    240                     return;
    241                 }
    242 
    243                 Ok(stream) => stream,
    244             };
    245 
    246             let mut all_tool_calls: HashMap<u32, PartialToolCall> = HashMap::new();
    247 
    248             while let Some(token) = token_stream.next().await {
    249                 let token = match token {
    250                     Ok(token) => token,
    251                     Err(err) => {
    252                         tracing::error!("failed to get token: {err}");
    253                         let _ = tx.send(DaveApiResponse::Failed(err.to_string()));
    254                         return;
    255                     }
    256                 };
    257 
    258                 for choice in &token.choices {
    259                     let resp = &choice.delta;
    260 
    261                     // if we have tool call arg chunks, collect them here
    262                     if let Some(tool_calls) = &resp.tool_calls {
    263                         for tool in tool_calls {
    264                             let entry = all_tool_calls.entry(tool.index).or_default();
    265 
    266                             if let Some(id) = &tool.id {
    267                                 entry.id_mut().get_or_insert(id.clone());
    268                             }
    269 
    270                             if let Some(name) = tool.function.as_ref().and_then(|f| f.name.as_ref())
    271                             {
    272                                 entry.name_mut().get_or_insert(name.to_string());
    273                             }
    274 
    275                             if let Some(argchunk) =
    276                                 tool.function.as_ref().and_then(|f| f.arguments.as_ref())
    277                             {
    278                                 entry
    279                                     .arguments_mut()
    280                                     .get_or_insert_with(String::new)
    281                                     .push_str(argchunk);
    282                             }
    283                         }
    284                     }
    285 
    286                     if let Some(content) = &resp.content {
    287                         if let Err(err) = tx.send(DaveApiResponse::Token(content.to_owned())) {
    288                             tracing::error!("failed to send dave response token to ui: {err}");
    289                         }
    290                         ctx.request_repaint();
    291                     }
    292                 }
    293             }
    294 
    295             let mut parsed_tool_calls = vec![];
    296             for (_index, partial) in all_tool_calls {
    297                 let Some(unknown_tool_call) = partial.complete() else {
    298                     tracing::error!("could not complete partial tool call: {:?}", partial);
    299                     continue;
    300                 };
    301 
    302                 match unknown_tool_call.parse(&tools) {
    303                     Ok(tool_call) => {
    304                         parsed_tool_calls.push(tool_call);
    305                     }
    306                     Err(err) => {
    307                         // TODO: we should be
    308                         tracing::error!(
    309                             "failed to parse tool call {:?}: {}",
    310                             unknown_tool_call,
    311                             err,
    312                         );
    313 
    314                         if let Some(id) = partial.id() {
    315                             // we have an id, so we can communicate the error
    316                             // back to the ai
    317                             parsed_tool_calls.push(ToolCall::invalid(
    318                                 id.to_string(),
    319                                 partial.name,
    320                                 partial.arguments,
    321                                 err.to_string(),
    322                             ));
    323                         }
    324                     }
    325                 };
    326             }
    327 
    328             if !parsed_tool_calls.is_empty() {
    329                 tx.send(DaveApiResponse::ToolCalls(parsed_tool_calls))
    330                     .unwrap();
    331                 ctx.request_repaint();
    332             }
    333 
    334             tracing::debug!("stream closed");
    335         });
    336     }
    337 }
    338 
    339 impl notedeck::App for Dave {
    340     fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
    341         let mut app_action: Option<AppAction> = None;
    342 
    343         // always insert system prompt if we have no context
    344         if self.chat.is_empty() {
    345             self.chat.push(Dave::system_prompt());
    346         }
    347 
    348         //update_dave(self, ctx, ui.ctx());
    349         let should_send = self.process_events(ctx);
    350         if let Some(action) = self.ui(ctx, ui).action {
    351             match action {
    352                 DaveAction::ToggleChrome => {
    353                     app_action = Some(AppAction::ToggleChrome);
    354                 }
    355                 DaveAction::Note(n) => {
    356                     app_action = Some(AppAction::Note(n));
    357                 }
    358                 DaveAction::NewChat => {
    359                     self.handle_new_chat();
    360                 }
    361                 DaveAction::Send => {
    362                     self.handle_user_send(ctx, ui);
    363                 }
    364             }
    365         }
    366 
    367         if should_send {
    368             self.send_user_message(ctx, ui.ctx());
    369         }
    370 
    371         AppResponse::action(app_action)
    372     }
    373 }