Cleanup unused message embedding code
Fixup some warnings
This commit is contained in:
@@ -72,11 +72,12 @@ pub fn strip_summary_boilerplate(summary: &str) -> String {
|
||||
|
||||
// Remove any remaining leading markdown bold markers
|
||||
if text.starts_with("**")
|
||||
&& let Some(end) = text[2..].find("**") {
|
||||
// Keep the content between ** but remove the markers
|
||||
let bold_content = &text[2..2 + end];
|
||||
text = format!("{}{}", bold_content, &text[4 + end..]);
|
||||
}
|
||||
&& let Some(end) = text[2..].find("**")
|
||||
{
|
||||
// Keep the content between ** but remove the markers
|
||||
let bold_content = &text[2..2 + end];
|
||||
text = format!("{}{}", bold_content, &text[4 + end..]);
|
||||
}
|
||||
|
||||
text.trim().to_string()
|
||||
}
|
||||
@@ -141,10 +142,7 @@ pub async fn generate_daily_summaries(
|
||||
if let Some(dt) = msg_dt {
|
||||
let date = dt.date_naive();
|
||||
if date >= start && date <= end {
|
||||
messages_by_date
|
||||
.entry(date)
|
||||
.or_default()
|
||||
.push(msg);
|
||||
messages_by_date.entry(date).or_default().push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
use crate::ai::{OllamaClient, SmsApiClient};
|
||||
use crate::database::{EmbeddingDao, InsertMessageEmbedding};
|
||||
|
||||
/// Background job to embed messages for a specific contact
|
||||
/// This function is idempotent - it checks if embeddings already exist before processing
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `contact` - The contact name to embed messages for (e.g., "Amanda")
|
||||
/// * `ollama` - Ollama client for generating embeddings
|
||||
/// * `sms_client` - SMS API client for fetching messages
|
||||
/// * `embedding_dao` - DAO for storing embeddings in the database
|
||||
///
|
||||
/// # Returns
|
||||
/// Ok(()) on success, Err on failure
|
||||
pub async fn embed_contact_messages(
|
||||
contact: &str,
|
||||
ollama: &OllamaClient,
|
||||
sms_client: &SmsApiClient,
|
||||
embedding_dao: Arc<Mutex<Box<dyn EmbeddingDao>>>,
|
||||
) -> Result<()> {
|
||||
log::info!("Starting message embedding job for contact: {}", contact);
|
||||
|
||||
let otel_context = opentelemetry::Context::new();
|
||||
|
||||
// Check existing embeddings count
|
||||
let existing_count = {
|
||||
let mut dao = embedding_dao.lock().expect("Unable to lock EmbeddingDao");
|
||||
dao.get_message_count(&otel_context, contact).unwrap_or(0)
|
||||
};
|
||||
|
||||
if existing_count > 0 {
|
||||
log::info!(
|
||||
"Contact '{}' already has {} embeddings, will check for new messages to embed",
|
||||
contact,
|
||||
existing_count
|
||||
);
|
||||
}
|
||||
|
||||
log::info!("Fetching all messages for contact: {}", contact);
|
||||
|
||||
// Fetch all messages for the contact
|
||||
let messages = sms_client.fetch_all_messages_for_contact(contact).await?;
|
||||
|
||||
let total_messages = messages.len();
|
||||
log::info!(
|
||||
"Fetched {} messages for contact '{}'",
|
||||
total_messages,
|
||||
contact
|
||||
);
|
||||
|
||||
if total_messages == 0 {
|
||||
log::warn!(
|
||||
"No messages found for contact '{}', nothing to embed",
|
||||
contact
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Filter out messages that already have embeddings and short/generic messages
|
||||
log::info!("Filtering out messages that already have embeddings and short messages...");
|
||||
let min_message_length = 30; // Skip short messages like "Thanks!" or "Yeah, it was :)"
|
||||
let messages_to_embed: Vec<&crate::ai::SmsMessage> = {
|
||||
let mut dao = embedding_dao.lock().expect("Unable to lock EmbeddingDao");
|
||||
messages
|
||||
.iter()
|
||||
.filter(|msg| {
|
||||
// Filter out short messages
|
||||
if msg.body.len() < min_message_length {
|
||||
return false;
|
||||
}
|
||||
// Filter out already embedded messages
|
||||
!dao.message_exists(&otel_context, contact, &msg.body, msg.timestamp)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let skipped = total_messages - messages_to_embed.len();
|
||||
let to_embed = messages_to_embed.len();
|
||||
|
||||
log::info!(
|
||||
"Found {} messages to embed ({} already embedded)",
|
||||
to_embed,
|
||||
skipped
|
||||
);
|
||||
|
||||
if to_embed == 0 {
|
||||
log::info!("All messages already embedded for contact '{}'", contact);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Process messages in batches
|
||||
let batch_size = 128; // Embed 128 messages per API call
|
||||
let mut successful = 0;
|
||||
let mut failed = 0;
|
||||
|
||||
for (batch_idx, batch) in messages_to_embed.chunks(batch_size).enumerate() {
|
||||
let batch_start = batch_idx * batch_size;
|
||||
let batch_end = batch_start + batch.len();
|
||||
|
||||
log::info!(
|
||||
"Processing batch {}/{}: messages {}-{} ({:.1}% complete)",
|
||||
batch_idx + 1,
|
||||
to_embed.div_ceil(batch_size),
|
||||
batch_start + 1,
|
||||
batch_end,
|
||||
(batch_end as f64 / to_embed as f64) * 100.0
|
||||
);
|
||||
|
||||
match embed_message_batch(batch, contact, ollama, embedding_dao.clone()).await {
|
||||
Ok(count) => {
|
||||
successful += count;
|
||||
log::debug!("Successfully embedded {} messages in batch", count);
|
||||
}
|
||||
Err(e) => {
|
||||
failed += batch.len();
|
||||
log::error!("Failed to embed batch: {:?}", e);
|
||||
// Continue processing despite failures
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between batches to avoid overwhelming Ollama
|
||||
if batch_end < to_embed {
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Message embedding job complete for '{}': {}/{} new embeddings created ({} already embedded, {} failed)",
|
||||
contact,
|
||||
successful,
|
||||
total_messages,
|
||||
skipped,
|
||||
failed
|
||||
);
|
||||
|
||||
if failed > 0 {
|
||||
log::warn!(
|
||||
"{} messages failed to embed for contact '{}'",
|
||||
failed,
|
||||
contact
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Embed a batch of messages using a single API call
|
||||
/// Returns the number of successfully embedded messages
|
||||
async fn embed_message_batch(
|
||||
messages: &[&crate::ai::SmsMessage],
|
||||
contact: &str,
|
||||
ollama: &OllamaClient,
|
||||
embedding_dao: Arc<Mutex<Box<dyn EmbeddingDao>>>,
|
||||
) -> Result<usize> {
|
||||
if messages.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Collect message bodies for batch embedding
|
||||
let bodies: Vec<&str> = messages.iter().map(|m| m.body.as_str()).collect();
|
||||
|
||||
// Generate embeddings for all messages in one API call
|
||||
let embeddings = ollama.generate_embeddings(&bodies).await?;
|
||||
|
||||
if embeddings.len() != messages.len() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Embedding count mismatch: got {} embeddings for {} messages",
|
||||
embeddings.len(),
|
||||
messages.len()
|
||||
));
|
||||
}
|
||||
|
||||
// Build batch of insert records
|
||||
let otel_context = opentelemetry::Context::new();
|
||||
let created_at = Utc::now().timestamp();
|
||||
let mut inserts = Vec::with_capacity(messages.len());
|
||||
|
||||
for (message, embedding) in messages.iter().zip(embeddings.iter()) {
|
||||
// Validate embedding dimensions
|
||||
if embedding.len() != 768 {
|
||||
log::warn!(
|
||||
"Invalid embedding dimensions: {} (expected 768), skipping",
|
||||
embedding.len()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
inserts.push(InsertMessageEmbedding {
|
||||
contact: contact.to_string(),
|
||||
body: message.body.clone(),
|
||||
timestamp: message.timestamp,
|
||||
is_sent: message.is_sent,
|
||||
embedding: embedding.clone(),
|
||||
created_at,
|
||||
model_version: "nomic-embed-text:v1.5".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Store all embeddings in a single transaction
|
||||
let mut dao = embedding_dao.lock().expect("Unable to lock EmbeddingDao");
|
||||
let stored_count = dao
|
||||
.store_message_embeddings_batch(&otel_context, inserts)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to store embeddings batch: {:?}", e))?;
|
||||
|
||||
Ok(stored_count)
|
||||
}
|
||||
@@ -86,9 +86,10 @@ impl InsightGenerator {
|
||||
// If path has at least 2 components (directory + file), extract first directory
|
||||
if components.len() >= 2
|
||||
&& let Some(component) = components.first()
|
||||
&& let Some(os_str) = component.as_os_str().to_str() {
|
||||
return Some(os_str.to_string());
|
||||
}
|
||||
&& let Some(os_str) = component.as_os_str().to_str()
|
||||
{
|
||||
return Some(os_str.to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -190,20 +191,19 @@ impl InsightGenerator {
|
||||
.filter(|msg| {
|
||||
// Extract date from formatted daily summary "[2024-08-15] Contact ..."
|
||||
if let Some(bracket_end) = msg.find(']')
|
||||
&& let Some(date_str) = msg.get(1..bracket_end) {
|
||||
// Parse just the date (daily summaries don't have time)
|
||||
if let Ok(msg_date) =
|
||||
chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
|
||||
{
|
||||
let msg_timestamp = msg_date
|
||||
.and_hms_opt(12, 0, 0)
|
||||
.unwrap()
|
||||
.and_utc()
|
||||
.timestamp();
|
||||
let time_diff = (photo_timestamp - msg_timestamp).abs();
|
||||
return time_diff > exclusion_window;
|
||||
}
|
||||
&& let Some(date_str) = msg.get(1..bracket_end)
|
||||
{
|
||||
// Parse just the date (daily summaries don't have time)
|
||||
if let Ok(msg_date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
|
||||
let msg_timestamp = msg_date
|
||||
.and_hms_opt(12, 0, 0)
|
||||
.unwrap()
|
||||
.and_utc()
|
||||
.timestamp();
|
||||
let time_diff = (photo_timestamp - msg_timestamp).abs();
|
||||
return time_diff > exclusion_window;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
.take(limit)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod daily_summary_job;
|
||||
pub mod embedding_job;
|
||||
pub mod handlers;
|
||||
pub mod insight_generator;
|
||||
pub mod ollama;
|
||||
|
||||
@@ -79,10 +79,11 @@ impl OllamaClient {
|
||||
{
|
||||
let cache = MODEL_LIST_CACHE.lock().unwrap();
|
||||
if let Some(entry) = cache.get(url)
|
||||
&& !entry.is_expired() {
|
||||
log::debug!("Returning cached model list for {}", url);
|
||||
return Ok(entry.data.clone());
|
||||
}
|
||||
&& !entry.is_expired()
|
||||
{
|
||||
log::debug!("Returning cached model list for {}", url);
|
||||
return Ok(entry.data.clone());
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Fetching fresh model list from {}", url);
|
||||
@@ -188,10 +189,11 @@ impl OllamaClient {
|
||||
{
|
||||
let cache = MODEL_CAPABILITIES_CACHE.lock().unwrap();
|
||||
if let Some(entry) = cache.get(url)
|
||||
&& !entry.is_expired() {
|
||||
log::debug!("Returning cached model capabilities for {}", url);
|
||||
return Ok(entry.data.clone());
|
||||
}
|
||||
&& !entry.is_expired()
|
||||
{
|
||||
log::debug!("Returning cached model capabilities for {}", url);
|
||||
return Ok(entry.data.clone());
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Fetching fresh model capabilities from {}", url);
|
||||
@@ -420,8 +422,8 @@ Return ONLY the title, nothing else."#,
|
||||
)
|
||||
}
|
||||
} else if let Some(contact_name) = contact {
|
||||
format!(
|
||||
r#"Create a short title (maximum 8 words) about this moment:
|
||||
format!(
|
||||
r#"Create a short title (maximum 8 words) about this moment:
|
||||
|
||||
Date: {}
|
||||
Location: {}
|
||||
@@ -431,15 +433,15 @@ Return ONLY the title, nothing else."#,
|
||||
Use specific details from the context above. The photo is from a folder for {}, so they are likely related to this moment. If no specific details are available, use a simple descriptive title.
|
||||
|
||||
Return ONLY the title, nothing else."#,
|
||||
date.format("%B %d, %Y"),
|
||||
location_str,
|
||||
contact_name,
|
||||
sms_str,
|
||||
contact_name
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"Create a short title (maximum 8 words) about this moment:
|
||||
date.format("%B %d, %Y"),
|
||||
location_str,
|
||||
contact_name,
|
||||
sms_str,
|
||||
contact_name
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"Create a short title (maximum 8 words) about this moment:
|
||||
|
||||
Date: {}
|
||||
Location: {}
|
||||
@@ -448,11 +450,11 @@ Return ONLY the title, nothing else."#,
|
||||
Use specific details from the context above. If no specific details are available, use a simple descriptive title.
|
||||
|
||||
Return ONLY the title, nothing else."#,
|
||||
date.format("%B %d, %Y"),
|
||||
location_str,
|
||||
sms_str
|
||||
)
|
||||
};
|
||||
date.format("%B %d, %Y"),
|
||||
location_str,
|
||||
sms_str
|
||||
)
|
||||
};
|
||||
|
||||
let system = custom_system.unwrap_or("You are my long term memory assistant. Use only the information provided. Do not invent details.");
|
||||
|
||||
@@ -509,8 +511,8 @@ Analyze the image and use specific details from both the visual content and the
|
||||
)
|
||||
}
|
||||
} else if let Some(contact_name) = contact {
|
||||
format!(
|
||||
r#"Write a 1-3 paragraph description of this moment based on the available information:
|
||||
format!(
|
||||
r#"Write a 1-3 paragraph description of this moment based on the available information:
|
||||
|
||||
Date: {}
|
||||
Location: {}
|
||||
@@ -518,27 +520,27 @@ Analyze the image and use specific details from both the visual content and the
|
||||
Messages: {}
|
||||
|
||||
Use only the specific details provided above. The photo is from a folder for {}, so they are likely related to this moment. Mention people's names (especially {}), places, or activities if they appear in the context. Write in first person as Cameron with the tone of a journal entry. If limited information is available, keep it simple and factual. If the location is unknown omit it"#,
|
||||
date.format("%B %d, %Y"),
|
||||
location_str,
|
||||
contact_name,
|
||||
sms_str,
|
||||
contact_name,
|
||||
contact_name
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"Write a 1-3 paragraph description of this moment based on the available information:
|
||||
date.format("%B %d, %Y"),
|
||||
location_str,
|
||||
contact_name,
|
||||
sms_str,
|
||||
contact_name,
|
||||
contact_name
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"Write a 1-3 paragraph description of this moment based on the available information:
|
||||
|
||||
Date: {}
|
||||
Location: {}
|
||||
Messages: {}
|
||||
|
||||
Use only the specific details provided above. Mention people's names, places, or activities if they appear in the context. Write in first person as Cameron with the tone of a journal entry. If limited information is available, keep it simple and factual. If the location is unknown omit it"#,
|
||||
date.format("%B %d, %Y"),
|
||||
location_str,
|
||||
sms_str
|
||||
)
|
||||
};
|
||||
date.format("%B %d, %Y"),
|
||||
location_str,
|
||||
sms_str
|
||||
)
|
||||
};
|
||||
|
||||
let system = custom_system.unwrap_or("You are a memory refreshing assistant who is able to provide insights through analyzing past conversations. Use only the information provided. Do not invent details.");
|
||||
|
||||
@@ -642,15 +644,6 @@ Analyze the image and use specific details from both the visual content and the
|
||||
Ok(embeddings)
|
||||
}
|
||||
|
||||
/// Internal helper to try generating an embedding from a specific server
|
||||
async fn try_generate_embedding(&self, url: &str, model: &str, text: &str) -> Result<Vec<f32>> {
|
||||
let embeddings = self.try_generate_embeddings(url, model, &[text]).await?;
|
||||
embeddings
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("No embedding returned from Ollama"))
|
||||
}
|
||||
|
||||
/// Internal helper to try generating embeddings for multiple texts from a specific server
|
||||
async fn try_generate_embeddings(
|
||||
&self,
|
||||
@@ -730,12 +723,6 @@ pub struct ModelCapabilities {
|
||||
pub has_vision: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct OllamaEmbedRequest {
|
||||
model: String,
|
||||
input: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct OllamaBatchEmbedRequest {
|
||||
model: String,
|
||||
|
||||
Reference in New Issue
Block a user