insight-chat: bootstrap insight on first Discuss message + regenerate flag
Tap-Discuss-on-no-insight previously failed silently: ImageApi's /insights/chat/stream required an existing agentic insight, errored when missing, and emitted the failure as `event: error` — which the frontend SSE consumer ignored (it listens for `error_message`). This commit closes both gaps with a server-side state machine: - /insights/chat/stream now branches on insight presence. Missing insight (or `regenerate: true` in the body) → bootstrap path: builds [System(req.system_prompt), User(req.user_message + image)], runs the agentic loop, generates a title, persists a new row via store_insight (which auto-flips priors). Existing insight → continuation path (unchanged behaviour). - New `regenerate: bool` request field forces bootstrap even when an insight exists. Takes precedence over `amend`. - `done` SSE payload field-name alignment with Apollo's frontend convention: prompt_eval_count → prompt_tokens, eval_count → eval_tokens, num_ctx echo added. - `amended_insight_id` semantics broaden — now populated whenever the turn produced a new row (bootstrap, regenerate, or amend). Existing amend clients keep working unchanged; new clients get the new row's id for free. - `event: error` → `event: error_message` so frontend errors stop silently dropping. Refactor: extracted run_streaming_agentic_loop, build_chat_clients, and generate_title as shared helpers between bootstrap and continuation. Continuation path's outer logic moves to run_continuation_streaming with no behaviour change. Mobile-ready: any client (Apollo backend, mobile, future) sends one request to /insights/chat/stream and gets the right path. Apollo's proxy stays a dumb pipe. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -641,11 +641,18 @@ pub struct ChatTurnHttpRequest {
|
||||
#[serde(default)]
|
||||
pub max_iterations: Option<usize>,
|
||||
/// Per-turn system-prompt override. Ephemeral in append mode,
|
||||
/// persisted in amend mode. See ChatTurnRequest for semantics.
|
||||
/// persisted in amend / regenerate mode. See ChatTurnRequest for
|
||||
/// semantics. Also seeds the bootstrap path when no insight exists.
|
||||
#[serde(default)]
|
||||
pub system_prompt: Option<String>,
|
||||
#[serde(default)]
|
||||
pub amend: bool,
|
||||
/// When true, force the bootstrap path even if an insight already
|
||||
/// exists: flip the existing row(s) to `is_current=false` and create
|
||||
/// a new insight row from this turn. Takes precedence over `amend`.
|
||||
/// Collapses to a normal bootstrap when no insight exists.
|
||||
#[serde(default)]
|
||||
pub regenerate: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -701,6 +708,7 @@ pub async fn chat_turn_handler(
|
||||
max_iterations: request.max_iterations,
|
||||
system_prompt: request.system_prompt.clone(),
|
||||
amend: request.amend,
|
||||
regenerate: request.regenerate,
|
||||
};
|
||||
|
||||
match app_state.insight_chat.chat_turn(chat_req).await {
|
||||
@@ -916,6 +924,7 @@ pub async fn chat_stream_handler(
|
||||
max_iterations: request.max_iterations,
|
||||
system_prompt: request.system_prompt.clone(),
|
||||
amend: request.amend,
|
||||
regenerate: request.regenerate,
|
||||
};
|
||||
|
||||
let service = app_state.insight_chat.clone();
|
||||
@@ -967,8 +976,9 @@ fn render_sse_frame(ev: &ChatStreamEvent) -> String {
|
||||
tool_calls_made,
|
||||
iterations_used,
|
||||
truncated,
|
||||
prompt_eval_count,
|
||||
eval_count,
|
||||
prompt_tokens,
|
||||
eval_tokens,
|
||||
num_ctx,
|
||||
amended_insight_id,
|
||||
backend_used,
|
||||
model_used,
|
||||
@@ -978,14 +988,20 @@ fn render_sse_frame(ev: &ChatStreamEvent) -> String {
|
||||
"tool_calls_made": tool_calls_made,
|
||||
"iterations_used": iterations_used,
|
||||
"truncated": truncated,
|
||||
"prompt_eval_count": prompt_eval_count,
|
||||
"eval_count": eval_count,
|
||||
"prompt_tokens": prompt_tokens,
|
||||
"eval_tokens": eval_tokens,
|
||||
"num_ctx": num_ctx,
|
||||
"amended_insight_id": amended_insight_id,
|
||||
"backend": backend_used,
|
||||
"model": model_used,
|
||||
}),
|
||||
),
|
||||
ChatStreamEvent::Error(msg) => ("error", serde_json::json!({ "message": msg })),
|
||||
// Apollo's frontend SSE consumer (and its free-chat backend, which
|
||||
// is the de-facto convention) listens for `error_message`. Emitting
|
||||
// `error` here meant any failure on the photo-chat path (e.g.
|
||||
// "no insight found for path") was silently dropped, leaving an
|
||||
// empty assistant bubble with no clue why the turn died.
|
||||
ChatStreamEvent::Error(msg) => ("error_message", serde_json::json!({ "message": msg })),
|
||||
};
|
||||
let data = serde_json::to_string(&payload).unwrap_or_else(|_| "{}".to_string());
|
||||
format!("event: {}\ndata: {}\n\n", event_name, data)
|
||||
|
||||
Reference in New Issue
Block a user