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 }