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