OpenRouter Support, Insight Chat and User injection #56

Merged
cameron merged 24 commits from 005-llm-client-trait into master 2026-04-26 23:01:35 +00:00
2 changed files with 59 additions and 12 deletions
Showing only changes of commit aa651d1c7b - Show all commits

View File

@@ -382,6 +382,12 @@ impl InsightChatService {
// 7. Append the new user turn.
messages.push(ChatMessage::user(req.user_message.clone()));
// Temporarily annotate the system message with this turn's iteration
// budget so the model knows how many tool-calling rounds it has. We
// restore the original content before persistence so the note doesn't
// accumulate across turns.
let original_system_content = annotate_system_with_budget(&mut messages, max_iterations);
let insight_cx = parent_cx.with_span(span);
// 8. Agentic loop — same shape as insight_generator's, but capped
@@ -468,6 +474,10 @@ impl InsightChatService {
loop_cx.span().set_status(Status::Ok);
// Drop the per-turn iteration-budget note from the system message
// before we persist so it doesn't snowball on each subsequent turn.
restore_system_content(&mut messages, original_system_content);
// 9. Persist. Append mode rewrites the JSON blob in place; amend
// mode regenerates the title and inserts a new insight row,
// relying on store_insight to flip prior rows' is_current=false.
@@ -789,6 +799,8 @@ impl InsightChatService {
messages.push(ChatMessage::user(req.user_message.clone()));
let original_system_content = annotate_system_with_budget(&mut messages, max_iterations);
let mut tool_calls_made = 0usize;
let mut iterations_used = 0usize;
let mut last_prompt_eval_count: Option<i32> = None;
@@ -917,6 +929,10 @@ impl InsightChatService {
messages.push(final_response);
}
// Drop the per-turn iteration-budget note from the system message
// before we persist so it doesn't snowball on each subsequent turn.
restore_system_content(&mut messages, original_system_content);
// Persist.
let json = serde_json::to_string(&messages)
.map_err(|e| anyhow!("failed to serialize chat history: {}", e))?;
@@ -1080,6 +1096,40 @@ fn env_max_iterations() -> usize {
.max(1)
}
/// Append a per-turn iteration-budget reminder to the replayed system
/// message so the model knows how many tool-calling rounds this turn gets.
/// Returns the original `content` so the caller can restore it before
/// persistence — otherwise the note would accumulate across turns.
///
/// No-op (returns `None`) when `messages` has no leading system message.
fn annotate_system_with_budget(
messages: &mut [ChatMessage],
max_iterations: usize,
) -> Option<String> {
let first = messages.first_mut()?;
if first.role != "system" {
return None;
}
let original = first.content.clone();
first.content = format!(
"{}\n\n(Budget for this chat turn: up to {} tool-calling iterations. Produce your final reply before the budget is exhausted.)",
first.content, max_iterations
);
Some(original)
}
/// Restore a system-message content previously captured by
/// [`annotate_system_with_budget`]. No-op when `original` is `None` or the
/// first message isn't a system message.
fn restore_system_content(messages: &mut [ChatMessage], original: Option<String>) {
let Some(original) = original else { return };
if let Some(first) = messages.first_mut()
&& first.role == "system"
{
first.content = original;
}
}
/// View returned to clients for chat-UI rendering.
#[derive(Debug)]
pub struct HistoryView {

View File

@@ -2892,8 +2892,11 @@ Return ONLY the summary, nothing else."#,
.collect()
};
// 6. Clear existing entity-photo links for this file so the run starts fresh,
// and ensure the owner entity exists so the agent can reference it.
// 6. Ensure the owner entity exists so the agent can reference it.
// Prior entity_photo_links for this file are intentionally preserved
// across regenerations — clearing them made `recall_facts_for_photo`
// always return empty and discarded hard-won knowledge. Re-linking
// the same entity is a no-op (INSERT OR IGNORE).
let owner_name = user_display_name();
let owner_entity_id: Option<i32> = {
let mut kdao = self
@@ -2901,14 +2904,6 @@ Return ONLY the summary, nothing else."#,
.lock()
.expect("Unable to lock KnowledgeDao");
if let Err(e) = kdao.delete_photo_links_for_file(&insight_cx, &file_path) {
log::warn!(
"Failed to clear entity_photo_links for {}: {:?}",
file_path,
e
);
}
// Upsert the owner entity so the agent always has a stable entity ID to reference.
let owner = crate::database::models::InsertEntity {
name: owner_name.clone(),
@@ -3000,9 +2995,11 @@ Return ONLY the summary, nothing else."#,
4. Use recall_entities to look up known people, places, or things that appear in this photo.\n\
5. When you identify people, places, events, or notable things in this photo: use store_entity to record them and store_fact to record key facts (relationships, roles, attributes). This builds a persistent memory for future insights.\n\
6. Only produce your final insight AFTER you have gathered context from at least 5 tool calls.\n\
7. If a tool returns no results, that is useful information — continue calling the remaining tools anyway.",
7. If a tool returns no results, that is useful information — continue calling the remaining tools anyway.\n\
8. You have a hard budget of {max_iterations} tool-calling iterations before the loop ends. Plan your context gathering so you can write a complete final insight within that budget.",
owner_id_note = owner_id_note,
owner_name = owner_name
owner_name = owner_name,
max_iterations = max_iterations
);
let system_content = if let Some(ref custom) = custom_system_prompt {
format!("{}\n\n{}", custom, base_system)