feat(ai): chat continuation for photo insights (server v1)
Adds POST /insights/chat and GET /insights/chat/history. Replays the stored agentic conversation through the same backend the insight was generated with (or a per-turn override), runs a short tool-calling loop, and persists the extended history in append or amend mode. Backend switching: same-backend or hybrid->local replay verbatim; local->hybrid is rejected in v1 (would require on-the-fly vision description rewrite). Per-(library, file) async mutex serialises concurrent turns. Soft context budget drops oldest tool_call+result pairs when the serialized history exceeds num_ctx - 2048 tokens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
29
src/state.rs
29
src/state.rs
@@ -1,3 +1,4 @@
|
||||
use crate::ai::insight_chat::{ChatLockMap, InsightChatService};
|
||||
use crate::ai::openrouter::OpenRouterClient;
|
||||
use crate::ai::{InsightGenerator, OllamaClient, SmsApiClient};
|
||||
use crate::database::{
|
||||
@@ -44,6 +45,8 @@ pub struct AppState {
|
||||
pub openrouter_allowed_models: Vec<String>,
|
||||
pub sms_client: SmsApiClient,
|
||||
pub insight_generator: InsightGenerator,
|
||||
/// Chat continuation service. Hold an Arc so handlers can clone cheaply.
|
||||
pub insight_chat: Arc<InsightChatService>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -76,6 +79,7 @@ impl AppState {
|
||||
openrouter_allowed_models: Vec<String>,
|
||||
sms_client: SmsApiClient,
|
||||
insight_generator: InsightGenerator,
|
||||
insight_chat: Arc<InsightChatService>,
|
||||
preview_dao: Arc<Mutex<Box<dyn PreviewDao>>>,
|
||||
) -> Self {
|
||||
assert!(
|
||||
@@ -109,6 +113,7 @@ impl AppState {
|
||||
openrouter_allowed_models,
|
||||
sms_client,
|
||||
insight_generator,
|
||||
insight_chat,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +204,18 @@ impl Default for AppState {
|
||||
libraries_vec.clone(),
|
||||
);
|
||||
|
||||
// Chat continuation reuses the generator for tool dispatch + image
|
||||
// loading. The lock map starts empty and grows lazily per file.
|
||||
let chat_locks: ChatLockMap =
|
||||
Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new()));
|
||||
let insight_chat = Arc::new(InsightChatService::new(
|
||||
Arc::new(insight_generator.clone()),
|
||||
ollama.clone(),
|
||||
openrouter.clone(),
|
||||
insight_dao.clone(),
|
||||
chat_locks,
|
||||
));
|
||||
|
||||
// Ensure preview clips directory exists
|
||||
let preview_clips_path =
|
||||
env::var("PREVIEW_CLIPS_DIRECTORY").unwrap_or_else(|_| "preview_clips".to_string());
|
||||
@@ -218,6 +235,7 @@ impl Default for AppState {
|
||||
openrouter_allowed_models,
|
||||
sms_client,
|
||||
insight_generator,
|
||||
insight_chat,
|
||||
preview_dao,
|
||||
)
|
||||
}
|
||||
@@ -320,6 +338,16 @@ impl AppState {
|
||||
vec![test_lib],
|
||||
);
|
||||
|
||||
let chat_locks: ChatLockMap =
|
||||
Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new()));
|
||||
let insight_chat = Arc::new(InsightChatService::new(
|
||||
Arc::new(insight_generator.clone()),
|
||||
ollama.clone(),
|
||||
None,
|
||||
insight_dao.clone(),
|
||||
chat_locks,
|
||||
));
|
||||
|
||||
// Initialize test preview DAO
|
||||
let preview_dao: Arc<Mutex<Box<dyn PreviewDao>>> =
|
||||
Arc::new(Mutex::new(Box::new(SqlitePreviewDao::new())));
|
||||
@@ -343,6 +371,7 @@ impl AppState {
|
||||
Vec::new(),
|
||||
sms_client,
|
||||
insight_generator,
|
||||
insight_chat,
|
||||
preview_dao,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user