notedeck

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

commit 6bbc20471a937a92517a9928f64804a7c9f45089
parent 093189b019af8628cdf5e2044d78a21245a634db
Author: William Casarin <jb55@jb55.com>
Date:   Thu,  1 May 2025 18:42:45 -0700

dave: include anonymous user identifier in api call

- don't include users pubkey

This could be used to associate requests with real users,
rendering the anonymized user_id pointless

TODO: Implement a new tool call that lets dave ask for your pubkey

Fixes: #834
Fixes: #836
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
MCargo.lock | 1+
Mcrates/notedeck/src/user_account.rs | 9++++++++-
Mcrates/notedeck_dave/Cargo.toml | 1+
Mcrates/notedeck_dave/src/lib.rs | 40++++++++++++++++++++++++++++++----------
4 files changed, 40 insertions(+), 11 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3281,6 +3281,7 @@ dependencies = [ "rand 0.9.0", "serde", "serde_json", + "sha2", "tokio", "tracing", ] diff --git a/crates/notedeck/src/user_account.rs b/crates/notedeck/src/user_account.rs @@ -1,4 +1,4 @@ -use enostr::Keypair; +use enostr::{Keypair, KeypairUnowned}; use tokenator::{ParseError, TokenParser, TokenSerializable}; use crate::wallet::ZapWallet; @@ -13,6 +13,13 @@ impl UserAccount { Self { key, wallet: None } } + pub fn keypair(&self) -> KeypairUnowned { + KeypairUnowned { + pubkey: &self.key.pubkey, + secret_key: self.key.secret_key.as_ref(), + } + } + pub fn with_wallet(mut self, wallet: ZapWallet) -> Self { self.wallet = Some(wallet); self diff --git a/crates/notedeck_dave/Cargo.toml b/crates/notedeck_dave/Cargo.toml @@ -6,6 +6,7 @@ version.workspace = true [dependencies] async-openai = "0.28.0" egui = { workspace = true } +sha2 = { workspace = true } notedeck = { workspace = true } notedeck_ui = { workspace = true } eframe = { workspace = true } diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -5,6 +5,7 @@ use async_openai::{ }; use chrono::{Duration, Local}; use egui_wgpu::RenderState; +use enostr::KeypairUnowned; use futures::StreamExt; use nostrdb::Transaction; use notedeck::{AppAction, AppContext}; @@ -37,20 +38,31 @@ pub struct Dave { /// A 3d representation of dave. avatar: Option<DaveAvatar>, input: String, - pubkey: String, tools: Arc<HashMap<String, Tool>>, client: async_openai::Client<OpenAIConfig>, incoming_tokens: Option<Receiver<DaveApiResponse>>, model_config: ModelConfig, } +/// Calculate an anonymous user_id from a keypair +fn calculate_user_id(keypair: KeypairUnowned) -> String { + use sha2::{Digest, Sha256}; + // pubkeys have degraded privacy, don't do that + let key_input = keypair + .secret_key + .map(|sk| sk.as_secret_bytes()) + .unwrap_or(keypair.pubkey.bytes()); + let hex_key = hex::encode(key_input); + let input = format!("{hex_key}notedeck_dave_user_id"); + hex::encode(Sha256::digest(input)) +} + impl Dave { pub fn avatar_mut(&mut self) -> Option<&mut DaveAvatar> { self.avatar.as_mut() } fn system_prompt() -> Message { - let pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245".to_string(); let now = Local::now(); let yesterday = now - Duration::hours(24); let date = now.format("%Y-%m-%d %H:%M:%S"); @@ -65,8 +77,6 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr - 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. -- The current users pubkey is {pubkey} - # Response Guidelines - 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,19 +94,18 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr let input = "".to_string(); let avatar = render_state.map(DaveAvatar::new); let mut tools: HashMap<String, Tool> = HashMap::new(); - let pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245".to_string(); for tool in tools::dave_tools() { tools.insert(tool.name().to_string(), tool); } + Dave { client, - pubkey: pubkey.clone(), avatar, incoming_tokens: None, tools: Arc::new(tools), input, model_config, - chat: vec![Self::system_prompt()], + chat: vec![], } } @@ -170,7 +179,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr } fn handle_new_chat(&mut self) { - self.chat = vec![Self::system_prompt()]; + self.chat = vec![]; self.input.clear(); } @@ -190,7 +199,13 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr .collect() }; tracing::debug!("sending messages, latest: {:?}", messages.last().unwrap()); - let pubkey = self.pubkey.clone(); + + let user_id = app_ctx + .accounts + .get_selected_account() + .map(|sa| calculate_user_id(sa.keypair())) + .unwrap_or_else(|| "unknown_user".to_string()); + let ctx = ctx.clone(); let client = self.client.clone(); let tools = self.tools.clone(); @@ -207,7 +222,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr stream: Some(true), messages, tools: Some(tools::dave_tools().iter().map(|t| t.to_api()).collect()), - user: Some(pubkey), + user: Some(user_id), ..Default::default() }) .await @@ -321,6 +336,11 @@ impl notedeck::App for Dave { */ let mut app_action: Option<AppAction> = None; + // always insert system prompt if we have no context + if self.chat.is_empty() { + self.chat.push(Dave::system_prompt()); + } + //update_dave(self, ctx, ui.ctx()); let should_send = self.process_events(ctx); if let Some(action) = self.ui(ctx, ui).action {