Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ff06413c6 | |||
| 66ea8490ab | |||
| 10ba706b39 | |||
| 9071d05932 |
@@ -48,6 +48,11 @@ pub struct GeneratePhotoInsightRequest {
|
|||||||
/// falls back to `DEFAULT_FEWSHOT_INSIGHT_IDS`.
|
/// falls back to `DEFAULT_FEWSHOT_INSIGHT_IDS`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub fewshot_insight_ids: Option<Vec<i32>>,
|
pub fewshot_insight_ids: Option<Vec<i32>>,
|
||||||
|
/// When true, drop `store_entity` / `store_fact` from the tool palette
|
||||||
|
/// for this run. Use for one-off explorations (caption-style prompts,
|
||||||
|
/// experimentation) that shouldn't pollute the persistent knowledge KB.
|
||||||
|
#[serde(default)]
|
||||||
|
pub disable_writes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -390,6 +395,7 @@ pub async fn generate_agentic_insight_handler(
|
|||||||
request.backend.clone(),
|
request.backend.clone(),
|
||||||
fewshot_examples,
|
fewshot_examples,
|
||||||
fewshot_ids,
|
fewshot_ids,
|
||||||
|
request.disable_writes,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -642,6 +648,10 @@ pub struct ChatTurnHttpRequest {
|
|||||||
pub max_iterations: Option<usize>,
|
pub max_iterations: Option<usize>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub amend: bool,
|
pub amend: bool,
|
||||||
|
/// Drop store_entity / store_fact from the tool palette for this turn —
|
||||||
|
/// useful for hypothetical/exploration chats that shouldn't pollute the KB.
|
||||||
|
#[serde(default)]
|
||||||
|
pub disable_writes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -696,6 +706,7 @@ pub async fn chat_turn_handler(
|
|||||||
min_p: request.min_p,
|
min_p: request.min_p,
|
||||||
max_iterations: request.max_iterations,
|
max_iterations: request.max_iterations,
|
||||||
amend: request.amend,
|
amend: request.amend,
|
||||||
|
disable_writes: request.disable_writes,
|
||||||
};
|
};
|
||||||
|
|
||||||
match app_state.insight_chat.chat_turn(chat_req).await {
|
match app_state.insight_chat.chat_turn(chat_req).await {
|
||||||
@@ -910,6 +921,7 @@ pub async fn chat_stream_handler(
|
|||||||
min_p: request.min_p,
|
min_p: request.min_p,
|
||||||
max_iterations: request.max_iterations,
|
max_iterations: request.max_iterations,
|
||||||
amend: request.amend,
|
amend: request.amend,
|
||||||
|
disable_writes: request.disable_writes,
|
||||||
};
|
};
|
||||||
|
|
||||||
let service = app_state.insight_chat.clone();
|
let service = app_state.insight_chat.clone();
|
||||||
|
|||||||
+19
-1
@@ -48,6 +48,10 @@ pub struct ChatTurnRequest {
|
|||||||
/// When true, write a new insight row (regenerating title) instead of
|
/// When true, write a new insight row (regenerating title) instead of
|
||||||
/// updating training_messages on the existing row.
|
/// updating training_messages on the existing row.
|
||||||
pub amend: bool,
|
pub amend: bool,
|
||||||
|
/// When true, drop `store_entity` / `store_fact` from the tool palette
|
||||||
|
/// for this turn. Use to explore alternate phrasings or run
|
||||||
|
/// hypothetical chats without polluting the persistent KB.
|
||||||
|
pub disable_writes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -362,6 +366,7 @@ impl InsightChatService {
|
|||||||
let tools = InsightGenerator::build_tool_definitions(
|
let tools = InsightGenerator::build_tool_definitions(
|
||||||
offer_describe_tool,
|
offer_describe_tool,
|
||||||
self.generator.apollo_enabled(),
|
self.generator.apollo_enabled(),
|
||||||
|
req.disable_writes,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Image base64 only needed when describe_photo is on the menu. Load
|
// Image base64 only needed when describe_photo is on the menu. Load
|
||||||
@@ -397,6 +402,9 @@ impl InsightChatService {
|
|||||||
// tighter and dispatching tools through the shared executor.
|
// tighter and dispatching tools through the shared executor.
|
||||||
let loop_span = tracer.start_with_context("ai.chat.loop", &insight_cx);
|
let loop_span = tracer.start_with_context("ai.chat.loop", &insight_cx);
|
||||||
let loop_cx = insight_cx.with_span(loop_span);
|
let loop_cx = insight_cx.with_span(loop_span);
|
||||||
|
// Memoize describe_photo for this turn so repeated calls don't
|
||||||
|
// produce conflicting visual descriptions in the assistant transcript.
|
||||||
|
let describe_cache: tokio::sync::Mutex<Option<String>> = tokio::sync::Mutex::new(None);
|
||||||
let mut tool_calls_made = 0usize;
|
let mut tool_calls_made = 0usize;
|
||||||
let mut iterations_used = 0usize;
|
let mut iterations_used = 0usize;
|
||||||
let mut last_prompt_eval_count: Option<i32> = None;
|
let mut last_prompt_eval_count: Option<i32> = None;
|
||||||
@@ -445,6 +453,7 @@ impl InsightChatService {
|
|||||||
&image_base64,
|
&image_base64,
|
||||||
&normalized,
|
&normalized,
|
||||||
&loop_cx,
|
&loop_cx,
|
||||||
|
Some(&describe_cache),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
messages.push(ChatMessage::tool_result(result));
|
messages.push(ChatMessage::tool_result(result));
|
||||||
@@ -793,6 +802,7 @@ impl InsightChatService {
|
|||||||
let tools = InsightGenerator::build_tool_definitions(
|
let tools = InsightGenerator::build_tool_definitions(
|
||||||
offer_describe_tool,
|
offer_describe_tool,
|
||||||
self.generator.apollo_enabled(),
|
self.generator.apollo_enabled(),
|
||||||
|
req.disable_writes,
|
||||||
);
|
);
|
||||||
|
|
||||||
let image_base64: Option<String> = if offer_describe_tool {
|
let image_base64: Option<String> = if offer_describe_tool {
|
||||||
@@ -814,6 +824,9 @@ impl InsightChatService {
|
|||||||
|
|
||||||
let original_system_content = annotate_system_with_budget(&mut messages, max_iterations);
|
let original_system_content = annotate_system_with_budget(&mut messages, max_iterations);
|
||||||
|
|
||||||
|
// Per-turn describe_photo memo, same intent as the non-streaming
|
||||||
|
// path: avoid replaying conflicting visual descriptions in transcript.
|
||||||
|
let describe_cache: tokio::sync::Mutex<Option<String>> = tokio::sync::Mutex::new(None);
|
||||||
let mut tool_calls_made = 0usize;
|
let mut tool_calls_made = 0usize;
|
||||||
let mut iterations_used = 0usize;
|
let mut iterations_used = 0usize;
|
||||||
let mut last_prompt_eval_count: Option<i32> = None;
|
let mut last_prompt_eval_count: Option<i32> = None;
|
||||||
@@ -889,6 +902,7 @@ impl InsightChatService {
|
|||||||
&image_base64,
|
&image_base64,
|
||||||
&normalized,
|
&normalized,
|
||||||
&cx,
|
&cx,
|
||||||
|
Some(&describe_cache),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (result_preview, result_truncated) = truncate_tool_result(&result);
|
let (result_preview, result_truncated) = truncate_tool_result(&result);
|
||||||
@@ -1134,8 +1148,12 @@ fn annotate_system_with_budget(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let original = first.content.clone();
|
let original = first.content.clone();
|
||||||
|
// Formatted as its own section so small models don't skim past it the
|
||||||
|
// way they tend to with parenthetical asides at the bottom of a long prompt.
|
||||||
|
// Phrasing matches the base prompt: budget = capacity, not a constraint
|
||||||
|
// to conserve. Small models otherwise tend to stop early.
|
||||||
first.content = format!(
|
first.content = format!(
|
||||||
"{}\n\n(Budget for this chat turn: up to {} tool-calling iterations. Produce your final reply before the budget is exhausted.)",
|
"{}\n\n## Budget for this chat turn\n\nYou have up to {} iterations available. Use as many as the question warrants for context-gathering, and reserve the last one for your reply.",
|
||||||
first.content, max_iterations
|
first.content, max_iterations
|
||||||
);
|
);
|
||||||
Some(original)
|
Some(original)
|
||||||
|
|||||||
+601
-140
File diff suppressed because it is too large
Load Diff
+12
-8
@@ -20,22 +20,24 @@ impl SmsApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch messages for a specific contact within ±4 days of the given timestamp
|
/// Fetch messages for a specific contact within ±`days_radius` days of
|
||||||
/// Falls back to all contacts if no messages found for the specific contact
|
/// the given timestamp (defaults to ±4 days when `None`). Falls back to
|
||||||
/// Messages are sorted by proximity to the center timestamp
|
/// all contacts if no messages are found for the specified contact.
|
||||||
|
/// Messages are sorted by proximity to the center timestamp.
|
||||||
pub async fn fetch_messages_for_contact(
|
pub async fn fetch_messages_for_contact(
|
||||||
&self,
|
&self,
|
||||||
contact: Option<&str>,
|
contact: Option<&str>,
|
||||||
center_timestamp: i64,
|
center_timestamp: i64,
|
||||||
|
days_radius: Option<i64>,
|
||||||
) -> Result<Vec<SmsMessage>> {
|
) -> Result<Vec<SmsMessage>> {
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
|
||||||
// Calculate ±4 days range around the center timestamp
|
let radius = days_radius.unwrap_or(4).clamp(1, 30);
|
||||||
let center_dt = chrono::DateTime::from_timestamp(center_timestamp, 0)
|
let center_dt = chrono::DateTime::from_timestamp(center_timestamp, 0)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Invalid timestamp"))?;
|
.ok_or_else(|| anyhow::anyhow!("Invalid timestamp"))?;
|
||||||
|
|
||||||
let start_dt = center_dt - Duration::days(4);
|
let start_dt = center_dt - Duration::days(radius);
|
||||||
let end_dt = center_dt + Duration::days(4);
|
let end_dt = center_dt + Duration::days(radius);
|
||||||
|
|
||||||
let start_ts = start_dt.timestamp();
|
let start_ts = start_dt.timestamp();
|
||||||
let end_ts = end_dt.timestamp();
|
let end_ts = end_dt.timestamp();
|
||||||
@@ -43,8 +45,9 @@ impl SmsApiClient {
|
|||||||
// If contact specified, try fetching for that contact first
|
// If contact specified, try fetching for that contact first
|
||||||
if let Some(contact_name) = contact {
|
if let Some(contact_name) = contact {
|
||||||
log::info!(
|
log::info!(
|
||||||
"Fetching SMS for contact: {} (±4 days from {})",
|
"Fetching SMS for contact: {} (±{} days from {})",
|
||||||
contact_name,
|
contact_name,
|
||||||
|
radius,
|
||||||
center_dt.format("%Y-%m-%d %H:%M:%S")
|
center_dt.format("%Y-%m-%d %H:%M:%S")
|
||||||
);
|
);
|
||||||
let messages = self
|
let messages = self
|
||||||
@@ -68,7 +71,8 @@ impl SmsApiClient {
|
|||||||
|
|
||||||
// Fallback to all contacts
|
// Fallback to all contacts
|
||||||
log::info!(
|
log::info!(
|
||||||
"Fetching all SMS messages (±4 days from {})",
|
"Fetching all SMS messages (±{} days from {})",
|
||||||
|
radius,
|
||||||
center_dt.format("%Y-%m-%d %H:%M:%S")
|
center_dt.format("%Y-%m-%d %H:%M:%S")
|
||||||
);
|
);
|
||||||
self.fetch_messages(start_ts, end_ts, None, Some(center_timestamp))
|
self.fetch_messages(start_ts, end_ts, None, Some(center_timestamp))
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
None,
|
None,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
|
false, // disable_writes — keep KB writes on for the population job
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|||||||
+6
-1
@@ -1718,7 +1718,12 @@ mod tests {
|
|||||||
// Mock — files.rs tests don't exercise the date-override endpoints.
|
// Mock — files.rs tests don't exercise the date-override endpoints.
|
||||||
// Returning a synthetic row keeps the trait satisfied without
|
// Returning a synthetic row keeps the trait satisfied without
|
||||||
// depending on private DbError constructors.
|
// depending on private DbError constructors.
|
||||||
Ok(mock_exif_row(library_id, rel_path, Some(date_taken), Some("manual".to_string())))
|
Ok(mock_exif_row(
|
||||||
|
library_id,
|
||||||
|
rel_path,
|
||||||
|
Some(date_taken),
|
||||||
|
Some("manual".to_string()),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_manual_date_taken(
|
fn clear_manual_date_taken(
|
||||||
|
|||||||
+3
-6
@@ -995,10 +995,8 @@ async fn upload_image(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let perceptual = perceptual_hash::compute(&uploaded_path);
|
let perceptual = perceptual_hash::compute(&uploaded_path);
|
||||||
let resolved_date = date_resolver::resolve_date_taken(
|
let resolved_date =
|
||||||
&uploaded_path,
|
date_resolver::resolve_date_taken(&uploaded_path, exif_data.date_taken);
|
||||||
exif_data.date_taken,
|
|
||||||
);
|
|
||||||
let insert_exif = InsertImageExif {
|
let insert_exif = InsertImageExif {
|
||||||
library_id: target_library.id,
|
library_id: target_library.id,
|
||||||
file_path: relative_path.clone(),
|
file_path: relative_path.clone(),
|
||||||
@@ -1022,8 +1020,7 @@ async fn upload_image(
|
|||||||
size_bytes,
|
size_bytes,
|
||||||
phash_64: perceptual.map(|h| h.phash_64),
|
phash_64: perceptual.map(|h| h.phash_64),
|
||||||
dhash_64: perceptual.map(|h| h.dhash_64),
|
dhash_64: perceptual.map(|h| h.dhash_64),
|
||||||
date_taken_source: resolved_date
|
date_taken_source: resolved_date.map(|r| r.source.as_str().to_string()),
|
||||||
.map(|r| r.source.as_str().to_string()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(mut dao) = exif_dao.lock() {
|
if let Ok(mut dao) = exif_dao.lock() {
|
||||||
|
|||||||
Reference in New Issue
Block a user