165 lines
5.4 KiB
Rust
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())
|
|
}
|