feature/insight-chat-improvements #83

Merged
cameron merged 19 commits from feature/insight-chat-improvements into master 2026-05-07 22:19:13 +00:00
3 changed files with 33 additions and 46 deletions
Showing only changes of commit e539c083c9 - Show all commits

View File

@@ -144,10 +144,13 @@ impl InsightGenerator {
} }
/// Compute the per-call tool gate options by probing each backing /// Compute the per-call tool gate options by probing each backing
/// table. Cheap (`SELECT 1 FROM <t> LIMIT 1` shape via the existing /// table for presence. `daily_summaries_present` uses a `LIMIT 1`
/// count methods); meant to be called once per chat turn / generation. /// existence probe; `calendar_present` and `location_history_present`
/// `has_vision` is supplied by the caller because it depends on the /// use the existing `get_event_count` / `get_location_count`
/// model selected for this turn, not on persistent state. /// methods (small enough that a full `COUNT(*)` is fine). Meant to
/// be called once per chat turn / generation. `has_vision` is
/// supplied by the caller because it depends on the model selected
/// for this turn, not on persistent state.
pub fn current_gate_opts(&self, has_vision: bool) -> ToolGateOpts { pub fn current_gate_opts(&self, has_vision: bool) -> ToolGateOpts {
let cx = opentelemetry::Context::new(); let cx = opentelemetry::Context::new();
let calendar_present = { let calendar_present = {
@@ -169,9 +172,7 @@ impl InsightGenerator {
.daily_summary_dao .daily_summary_dao
.lock() .lock()
.expect("Unable to lock DailySummaryDao"); .expect("Unable to lock DailySummaryDao");
dao.get_total_summary_count(&cx) dao.has_any_summaries(&cx).unwrap_or(false)
.map(|n| n > 0)
.unwrap_or(false)
}; };
ToolGateOpts { ToolGateOpts {
has_vision, has_vision,

View File

@@ -261,38 +261,16 @@ impl SmsApiClient {
/// - "fts5" keyword-only, supports phrase / prefix / boolean / NEAR /// - "fts5" keyword-only, supports phrase / prefix / boolean / NEAR
/// - "semantic" embedding similarity /// - "semantic" embedding similarity
/// - "hybrid" both merged via reciprocal rank fusion (recommended) /// - "hybrid" both merged via reciprocal rank fusion (recommended)
///
/// Equivalent to `search_messages_with_contact(query, mode, limit, None)`;
/// kept as a convenience for callers that don't filter by contact.
pub async fn search_messages( pub async fn search_messages(
&self, &self,
query: &str, query: &str,
mode: &str, mode: &str,
limit: usize, limit: usize,
) -> Result<Vec<SmsSearchHit>> { ) -> Result<Vec<SmsSearchHit>> {
let url = format!( self.search_messages_with_contact(query, mode, limit, None).await
"{}/api/messages/search/?q={}&mode={}&limit={}",
self.base_url,
urlencoding::encode(query),
urlencoding::encode(mode),
limit
);
let mut request = self.client.get(&url);
if let Some(token) = &self.token {
request = request.header("Authorization", format!("Bearer {}", token));
}
let response = request.send().await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(anyhow::anyhow!(
"SMS search request failed: {} - {}",
status,
body
));
}
let data: SmsSearchResponse = response.json().await?;
Ok(data.results)
} }
/// Same shape as `search_messages` but with optional `contact_id`. The /// Same shape as `search_messages` but with optional `contact_id`. The

View File

@@ -76,12 +76,13 @@ pub trait DailySummaryDao: Sync + Send {
contact: &str, contact: &str,
) -> Result<i64, DbError>; ) -> Result<i64, DbError>;
/// Get total count of all summaries (across all contacts). Used by /// Cheap presence check — returns true iff at least one daily summary row
/// `current_gate_opts` to check whether daily_summaries are present. /// exists. Used by gating logic that only needs "is the table empty?",
fn get_total_summary_count( /// avoiding a `COUNT(*)` full scan on large corpora.
fn has_any_summaries(
&mut self, &mut self,
context: &opentelemetry::Context, context: &opentelemetry::Context,
) -> Result<i64, DbError>; ) -> Result<bool, DbError>;
} }
pub struct SqliteDailySummaryDao { pub struct SqliteDailySummaryDao {
@@ -462,20 +463,27 @@ impl DailySummaryDao for SqliteDailySummaryDao {
.map_err(|_| DbError::new(DbErrorKind::QueryError)) .map_err(|_| DbError::new(DbErrorKind::QueryError))
} }
fn get_total_summary_count( fn has_any_summaries(&mut self, context: &opentelemetry::Context) -> Result<bool, DbError> {
&mut self, trace_db_call(context, "query", "has_any_summaries", |_span| {
context: &opentelemetry::Context,
) -> Result<i64, DbError> {
trace_db_call(context, "query", "get_total_summary_count", |_span| {
let mut conn = self let mut conn = self
.connection .connection
.lock() .lock()
.expect("Unable to get DailySummaryDao"); .expect("Unable to get DailySummaryDao");
diesel::sql_query("SELECT COUNT(*) as count FROM daily_conversation_summaries") #[derive(QueryableByName)]
.get_result::<CountResult>(conn.deref_mut()) struct ProbeResult {
.map(|r| r.count) #[diesel(sql_type = diesel::sql_types::Integer)]
.map_err(|e| anyhow::anyhow!("Count query error: {:?}", e)) #[allow(dead_code)]
one: i32,
}
let rows: Vec<ProbeResult> = diesel::sql_query(
"SELECT 1 as one FROM daily_conversation_summaries LIMIT 1",
)
.load(conn.deref_mut())
.map_err(|e| anyhow::anyhow!("Failed to probe daily summaries: {}", e))?;
Ok(!rows.is_empty())
}) })
.map_err(|_| DbError::new(DbErrorKind::QueryError)) .map_err(|_| DbError::new(DbErrorKind::QueryError))
} }