Files
sovereign_browser_rust/src/agent_conversations.rs
T

165 lines
5.4 KiB
Rust

//! Nostr conversation persistence for agent chat
//!
//! Port of `agent_conversations.c` / `agent_conversations.h` from the C project.
//! Uses `nostr_signer` from `rust_core_lib`.
use nostr_signer::traits::NostrSigner;
use std::sync::Arc;
use std::sync::Mutex;
use once_cell::sync::Lazy;
use crate::db;
use crate::agent_chat_store;
/// The Nostr kind for arbitrary custom app data (NIP-78).
const AGENT_CONV_KIND: u64 = 30078;
/// The t-tag that identifies conversation events.
const AGENT_CONV_T_TAG: &str = "client-ai-chat-v1";
/// Global state for conversation persistence.
static G_CONV_STATE: Lazy<Mutex<ConvState>> = Lazy::new(|| Mutex::new(ConvState::default()));
struct ConvState {
signer: Option<Arc<dyn NostrSigner>>,
pubkey_hex: String,
have_signer: bool,
}
impl Default for ConvState {
fn default() -> Self {
ConvState {
signer: None,
pubkey_hex: String::new(),
have_signer: false,
}
}
}
/// Initialize the conversation persistence module.
pub fn agent_conversations_init(signer: Option<Arc<dyn NostrSigner>>, pubkey_hex: &str) {
let mut state = G_CONV_STATE.lock().unwrap();
state.signer = signer;
state.pubkey_hex = pubkey_hex.to_string();
state.have_signer = state.signer.is_some() && !state.pubkey_hex.is_empty();
}
/// Update the signer reference.
pub fn agent_conversations_set_signer(signer: Option<Arc<dyn NostrSigner>>, pubkey_hex: &str) {
agent_conversations_init(signer, pubkey_hex);
}
/// List all saved conversations from the local SQLite cache.
pub fn agent_conversations_list() -> Vec<serde_json::Value> {
match db::db_query_events(&[AGENT_CONV_KIND], None, None, None, None) {
Ok(events) => {
let mut result = Vec::new();
for event in events {
let tags = event["tags"].as_array();
let has_conv_tag = tags.map_or(false, |t| {
t.iter().any(|tag| {
tag.as_array()
.map_or(false, |a| a.len() >= 2 && a[0] == "t" && a[1] == AGENT_CONV_T_TAG)
})
});
if has_conv_tag {
let conv_id = tags.and_then(|t| {
t.iter().find_map(|tag| {
tag.as_array().and_then(|a| {
if a.len() >= 2 && a[0] == "d" {
Some(a[1].as_str().unwrap_or("").to_string())
} else {
None
}
})
})
}).unwrap_or_default();
result.push(serde_json::json!({
"id": conv_id,
"title": event["content"].as_str().unwrap_or("Untitled"),
"updated_at": event["created_at"],
}));
}
}
result
}
Err(_) => Vec::new(),
}
}
/// Save the current agent chat session to Nostr as a kind 30078 event.
pub fn agent_conversations_save(
conversation_id: Option<&str>,
title: Option<&str>,
) -> Option<String> {
let state = G_CONV_STATE.lock().unwrap();
if !state.have_signer {
return None;
}
let conv_id = conversation_id
.map(|s| s.to_string())
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let session = agent_chat_store::agent_chat_store_get_session();
let _title = title
.map(|s| s.to_string())
.unwrap_or_else(|| {
session.messages.first()
.map(|m| {
let truncated: String = m.content.chars().take(60).collect();
truncated
})
.unwrap_or_else(|| "Untitled".to_string())
});
println!(
"[conversations] Save requested: id={} (NIP-44 encryption not yet implemented)",
conv_id
);
Some(conv_id)
}
/// Load a conversation from the local SQLite cache.
pub fn agent_conversations_load(conversation_id: &str) -> Result<(), Box<dyn std::error::Error>> {
let events = db::db_query_events(
&[AGENT_CONV_KIND],
None,
None,
None,
Some(1),
)?;
for event in events {
let tags = event["tags"].as_array();
let has_id = tags.map_or(false, |t| {
t.iter().any(|tag| {
tag.as_array()
.map_or(false, |a| a.len() >= 2 && a[0] == "d" && a[1] == conversation_id)
})
});
if has_id {
if let Some(content_str) = event["content"].as_str() {
if let Ok(data) = serde_json::from_str::<serde_json::Value>(content_str) {
if let Some(messages) = data["messages"].as_array() {
let msgs: Vec<agent_chat_store::ChatMessage> =
serde_json::from_value(serde_json::Value::Array(messages.clone()))?;
let session = agent_chat_store::ChatSession {
messages: msgs,
system_prompt: String::new(),
};
agent_chat_store::agent_chat_store_load_session(session);
return Ok(());
}
}
}
}
}
Err("Conversation not found".into())
}