Create Insight Generation Feature
Added integration with Messages API and Ollama
This commit is contained in:
220
src/ai/sms_client.rs
Normal file
220
src/ai/sms_client.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use anyhow::Result;
|
||||
use chrono::NaiveDate;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::ollama::OllamaClient;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SmsApiClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
impl SmsApiClient {
|
||||
pub fn new(base_url: String, token: Option<String>) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
base_url,
|
||||
token,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_messages_for_date(&self, date: NaiveDate) -> Result<Vec<SmsMessage>> {
|
||||
// Calculate date range (midnight to midnight in local time)
|
||||
let start = date
|
||||
.and_hms_opt(0, 0, 0)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid start time"))?;
|
||||
let end = date
|
||||
.and_hms_opt(23, 59, 59)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid end time"))?;
|
||||
|
||||
let start_ts = start.and_utc().timestamp();
|
||||
let end_ts = end.and_utc().timestamp();
|
||||
|
||||
self.fetch_messages(start_ts, end_ts, None, None).await
|
||||
}
|
||||
|
||||
/// Fetch messages for a specific contact within ±1 day of the given timestamp
|
||||
/// Falls back to all contacts if no messages found for the specific contact
|
||||
/// Messages are sorted by proximity to the center timestamp
|
||||
pub async fn fetch_messages_for_contact(
|
||||
&self,
|
||||
contact: Option<&str>,
|
||||
center_timestamp: i64,
|
||||
) -> Result<Vec<SmsMessage>> {
|
||||
use chrono::Duration;
|
||||
|
||||
// Calculate ±1 day range around the center timestamp
|
||||
let center_dt = chrono::DateTime::from_timestamp(center_timestamp, 0)
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid timestamp"))?;
|
||||
|
||||
let start_dt = center_dt - Duration::days(1);
|
||||
let end_dt = center_dt + Duration::days(1);
|
||||
|
||||
let start_ts = start_dt.timestamp();
|
||||
let end_ts = end_dt.timestamp();
|
||||
|
||||
// If contact specified, try fetching for that contact first
|
||||
if let Some(contact_name) = contact {
|
||||
log::info!(
|
||||
"Fetching SMS for contact: {} (±1 day from {})",
|
||||
contact_name,
|
||||
center_dt.format("%Y-%m-%d %H:%M:%S")
|
||||
);
|
||||
let messages = self
|
||||
.fetch_messages(start_ts, end_ts, Some(contact_name), Some(center_timestamp))
|
||||
.await?;
|
||||
|
||||
if !messages.is_empty() {
|
||||
log::info!(
|
||||
"Found {} messages for contact {}",
|
||||
messages.len(),
|
||||
contact_name
|
||||
);
|
||||
return Ok(messages);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"No messages found for contact {}, falling back to all contacts",
|
||||
contact_name
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to all contacts
|
||||
log::info!(
|
||||
"Fetching all SMS messages (±1 day from {})",
|
||||
center_dt.format("%Y-%m-%d %H:%M:%S")
|
||||
);
|
||||
self.fetch_messages(start_ts, end_ts, None, Some(center_timestamp))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Internal method to fetch messages with optional contact filter and timestamp sorting
|
||||
async fn fetch_messages(
|
||||
&self,
|
||||
start_ts: i64,
|
||||
end_ts: i64,
|
||||
contact: Option<&str>,
|
||||
center_timestamp: Option<i64>,
|
||||
) -> Result<Vec<SmsMessage>> {
|
||||
// Call Django endpoint
|
||||
let mut url = format!(
|
||||
"{}/api/messages/by-date-range/?start_date={}&end_date={}",
|
||||
self.base_url, start_ts, end_ts
|
||||
);
|
||||
|
||||
// Add contact filter if provided
|
||||
if let Some(contact_name) = contact {
|
||||
url.push_str(&format!("&contact={}", urlencoding::encode(contact_name)));
|
||||
}
|
||||
|
||||
// Add timestamp for proximity sorting if provided
|
||||
if let Some(ts) = center_timestamp {
|
||||
url.push_str(&format!("×tamp={}", ts));
|
||||
}
|
||||
|
||||
log::debug!("Fetching SMS messages from: {}", url);
|
||||
|
||||
let mut request = self.client.get(&url);
|
||||
|
||||
// Add authorization header if token exists
|
||||
if let Some(token) = &self.token {
|
||||
request = request.header("Authorization", format!("Bearer {}", token));
|
||||
}
|
||||
|
||||
let response = request.send().await?;
|
||||
|
||||
log::debug!("SMS API response status: {}", response.status());
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let error_body = response.text().await.unwrap_or_default();
|
||||
log::error!("SMS API request failed: {} - {}", status, error_body);
|
||||
return Err(anyhow::anyhow!(
|
||||
"SMS API request failed: {} - {}",
|
||||
status,
|
||||
error_body
|
||||
));
|
||||
}
|
||||
|
||||
let data: SmsApiResponse = response.json().await?;
|
||||
|
||||
// Convert to internal format
|
||||
Ok(data
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|m| SmsMessage {
|
||||
contact: m.contact_name,
|
||||
body: m.body,
|
||||
timestamp: m.date,
|
||||
is_sent: m.type_ == 2, // type 2 = sent
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn summarize_context(
|
||||
&self,
|
||||
messages: &[SmsMessage],
|
||||
ollama: &OllamaClient,
|
||||
) -> Result<String> {
|
||||
if messages.is_empty() {
|
||||
return Ok(String::from("No messages on this day"));
|
||||
}
|
||||
|
||||
// Create prompt for Ollama with sender/receiver distinction
|
||||
let messages_text: String = messages
|
||||
.iter()
|
||||
.take(60) // Limit to avoid token overflow
|
||||
.map(|m| {
|
||||
if m.is_sent {
|
||||
format!("Me: {}", m.body)
|
||||
} else {
|
||||
format!("{}: {}", m.contact, m.body)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
let prompt = format!(
|
||||
r#"Summarize these messages in up to 4-5 sentences. Focus on key topics, places, people mentioned, and the overall context of the conversations.
|
||||
|
||||
Messages:
|
||||
{}
|
||||
|
||||
Summary:"#,
|
||||
messages_text
|
||||
);
|
||||
|
||||
ollama
|
||||
.generate(
|
||||
&prompt,
|
||||
// Some("You are a summarizer for the purposes of jogging my memory and highlighting events and situations."),
|
||||
Some("You are the keeper of memories, ingest the context and give me a casual summary of the moment."),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmsMessage {
|
||||
pub contact: String,
|
||||
pub body: String,
|
||||
pub timestamp: i64,
|
||||
pub is_sent: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SmsApiResponse {
|
||||
messages: Vec<SmsApiMessage>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SmsApiMessage {
|
||||
contact_name: String,
|
||||
body: String,
|
||||
date: i64,
|
||||
#[serde(rename = "type")]
|
||||
type_: i32,
|
||||
}
|
||||
Reference in New Issue
Block a user