Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/src/app/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ macro_rules! invoke_handler {
crate::storage_manager::group_sessions::group_session_toggle_memory_pin,
crate::storage_manager::group_sessions::group_session_set_memory_cold_state,
crate::group_chat_manager::group_chat_add_user_message,
crate::group_chat_manager::group_chat_message_debug_snapshot,
crate::group_chat_manager::group_chat_send,
crate::group_chat_manager::group_chat_regenerate,
crate::group_chat_manager::group_chat_continue,
Expand Down
89 changes: 86 additions & 3 deletions src-tauri/src/chat_manager/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ use super::types::{
ChatGenerateLorebookEntryDraftArgs, ChatGenerateLorebookKeywordDraftArgs,
ChatGenerateSceneImageArgs, ChatGenerateScenePromptArgs, ChatRegenerateArgs, ChatTurnResult,
ContinueResult, ImageAttachment, LorebookEntryDraftResult, LorebookKeywordDraftResult,
PromptTemplateType, RegenerateResult, Session, Settings, StoredMessage, SystemPromptEntry,
SystemPromptTemplate,
MemoryEmbedding, PromptTemplateType, RegenerateResult, Session, Settings, StoredMessage,
SystemPromptEntry, SystemPromptTemplate,
};
use crate::storage_manager::sessions::{messages_upsert_batch_typed, session_upsert_meta_typed};

Expand Down Expand Up @@ -67,6 +67,26 @@ pub struct ChatMessageDebugSnapshot {
pub request_messages: Vec<Value>,
pub request_body: Value,
pub notes: Vec<String>,
// Raw contents of the individual prompt placeholders. Present so the frontend
// token breakdown can attribute tokens to character profile / persona /
// lorebook / memories / author note / companion state / scheduled notes even
// when a template renders them inline (i.e. they never appear as standalone
// prompt entries).
pub character_profile_content: String,
pub persona_content: String,
pub memory_entry_count: u32,
pub lorebook_content: String,
pub context_summary_content: String,
pub key_memories_content: String,
pub author_note_content: String,
pub companion_state_content: String,
pub scheduled_notes_content: String,
// Raw text of the `{{group_characters}}` cast block. Only populated for group
// chat snapshots; empty for 1:1 chats where that placeholder does not exist.
pub group_cast_content: String,
// Group chat only: number of other participants whose profiles are in the
// group cast block (0 for 1:1 chats).
pub group_cast_count: u32,
}

#[derive(Clone, Copy)]
Expand Down Expand Up @@ -455,6 +475,18 @@ pub async fn chat_continue(
.await
}

/// Strip an optional leading `<score>::` prefix from a stored memory ref,
/// returning the underlying memory text. Refs are persisted either as plain text
/// or as `"{match_score}::{text}"` (see completion/regenerate memory_refs).
fn memory_ref_text(reference: &str) -> &str {
if let Some((prefix, rest)) = reference.split_once("::") {
if prefix.trim().parse::<f64>().is_ok() {
return rest;
}
}
reference
}

#[tauri::command]
pub fn chat_message_debug_snapshot(
app: AppHandle,
Expand Down Expand Up @@ -510,14 +542,54 @@ pub fn chat_message_debug_snapshot(
}
};

// Reconstruct the memories that were injected into {{key_memories}} from the
// stored refs instead of running a live (async) retrieval, so the debug
// snapshot mirrors what generation actually shipped. Refs are persisted as
// plain text or as `"{match_score}::{text}"` (see completion/regenerate).
let retrieved_memories: Vec<MemoryEmbedding> = target_message
.memory_refs
.iter()
.filter_map(|reference| {
let text = memory_ref_text(reference);
prompt_session
.memory_embeddings
.iter()
.find(|memory| memory.id == *reference || memory.text == text)
.cloned()
.or_else(|| {
serde_json::from_value(json!({
"id": reference,
"text": text,
"embedding": Vec::<f32>::new(),
}))
.ok()
})
})
.collect();

let prompt_entries = append_image_directive_instructions(
context.build_system_prompt(&character, &model, persona.as_ref(), &prompt_session),
context.build_system_prompt(
&character,
&model,
persona.as_ref(),
&prompt_session,
&retrieved_memories,
),
&context.settings,
);
let (prompt_template_source, prompt_template_id, prompt_template_name) =
resolve_debug_prompt_template(&app, &prompt_session, &character, &context.settings);
let (relative_entries, in_chat_entries) = partition_prompt_entries(prompt_entries.clone());

let prompt_sources = prompt_engine::debug_prompt_source_contents(
&app,
&character,
persona.as_ref(),
&prompt_session,
&context.settings,
&retrieved_memories,
);

let system_role = crate::chat_manager::request_builder::system_role_for(&credential);
let character_name = character.name.as_str();
let persona_name = persona
Expand Down Expand Up @@ -641,6 +713,17 @@ pub fn chat_message_debug_snapshot(
request_messages,
request_body: built.body,
notes,
character_profile_content: prompt_sources.character_profile,
persona_content: prompt_sources.persona_description,
memory_entry_count: prompt_sources.memory_entry_count,
lorebook_content: prompt_sources.lorebook,
context_summary_content: prompt_sources.context_summary,
key_memories_content: prompt_sources.key_memories,
author_note_content: prompt_sources.author_note,
companion_state_content: prompt_sources.companion_state,
scheduled_notes_content: prompt_sources.scheduled_notes,
group_cast_content: String::new(),
group_cast_count: 0,
})
}

Expand Down
107 changes: 54 additions & 53 deletions src-tauri/src/chat_manager/flows/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ use crate::chat_manager::service::{
record_failed_usage, record_usage_if_available, ChatService, PreparedChatTurn,
};
use crate::chat_manager::storage::recent_messages;
use crate::chat_manager::temporal::{
companion_effective_now, companion_time_awareness_enabled, format_memory_for_prompt,
};
use crate::chat_manager::temporal::{companion_effective_now, companion_time_awareness_enabled};
use crate::chat_manager::turn_builder::{
append_image_directive_instructions, assemble_prompt_messages, build_enriched_query,
conversation_window_with_pinned, is_dynamic_memory_active, manual_window_size,
Expand Down Expand Up @@ -202,44 +200,10 @@ impl CompletionFlow {
}),
);

let mut prompt_entries = if swap_places {
let (prompt_character, prompt_persona) =
swapped_prompt_entities(&character, persona.as_ref());
append_image_directive_instructions(
context.build_system_prompt(
&prompt_character,
&model,
prompt_persona.as_ref(),
&session,
),
settings,
)
} else {
append_image_directive_instructions(
context.build_system_prompt(&character, &model, persona.as_ref(), &session),
settings,
)
};

let used_lorebook_entries =
crate::chat_manager::prompt_engine::resolve_used_lorebook_entries(
&app,
&character.id,
persona.as_ref(),
&session,
&prompt_entries,
);
let (pinned_msgs, recent_msgs) = if dynamic_memory_enabled {
let (pinned, unpinned) =
conversation_window_with_pinned(&session.messages, dynamic_window);
(pinned, unpinned)
} else {
(
Vec::new(),
recent_messages(&session, manual_window_size(settings)),
)
};

// Dynamic memory retrieval runs before build_system_prompt so the
// retrieved relevant memories fill {{key_memories}} (instead of shipping
// the entire hot memory bank). Retrieval also mutates session state
// (pin/promote/access bookkeeping), which build_system_prompt then reads.
let relevant_memories = if dynamic_memory_enabled && !session.memory_embeddings.is_empty() {
let fixed = ensure_pinned_hot(&mut session.memory_embeddings);
if fixed > 0 {
Expand Down Expand Up @@ -328,6 +292,51 @@ impl CompletionFlow {
}
}

let mut prompt_entries = if swap_places {
let (prompt_character, prompt_persona) =
swapped_prompt_entities(&character, persona.as_ref());
append_image_directive_instructions(
context.build_system_prompt(
&prompt_character,
&model,
prompt_persona.as_ref(),
&session,
&relevant_memories,
),
settings,
)
} else {
append_image_directive_instructions(
context.build_system_prompt(
&character,
&model,
persona.as_ref(),
&session,
&relevant_memories,
),
settings,
)
};

let used_lorebook_entries =
crate::chat_manager::prompt_engine::resolve_used_lorebook_entries(
&app,
&character.id,
persona.as_ref(),
&session,
&prompt_entries,
);
let (pinned_msgs, recent_msgs) = if dynamic_memory_enabled {
let (pinned, unpinned) =
conversation_window_with_pinned(&session.messages, dynamic_window);
(pinned, unpinned)
} else {
(
Vec::new(),
recent_messages(&session, manual_window_size(settings)),
)
};

let system_role = crate::chat_manager::request_builder::system_role_for(&credential);
if swap_places {
let persona_title = persona
Expand All @@ -337,19 +346,11 @@ impl CompletionFlow {
prompt_entries.push(swap_places_entry(&character.name, &persona_title));
}

// In dynamic mode the retrieved memories are now injected inline via
// {{key_memories}} inside build_system_prompt, so no separate block is
// needed here. The manual-memory path is unchanged.
let memory_block = if dynamic_memory_enabled {
if relevant_memories.is_empty() {
None
} else {
let memory_now = companion_effective_now(&session);
Some(
relevant_memories
.iter()
.map(|mem| format_memory_for_prompt(mem, memory_now))
.collect::<Vec<_>>()
.join("\n"),
)
}
None
} else if has_manual_memories(&session.memories) {
Some(render_manual_memory_lines(&session.memories))
} else {
Expand Down
9 changes: 8 additions & 1 deletion src-tauri/src/chat_manager/flows/continuation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,19 @@ impl ContinueFlow {
&model,
prompt_persona.as_ref(),
&session,
&relevant_memories,
),
settings,
)
} else {
append_image_directive_instructions(
context.build_system_prompt(&character, &model, persona.as_ref(), &session),
context.build_system_prompt(
&character,
&model,
persona.as_ref(),
&session,
&relevant_memories,
),
settings,
)
};
Expand Down
9 changes: 8 additions & 1 deletion src-tauri/src/chat_manager/flows/regenerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,12 +278,19 @@ impl RegenerateFlow {
&model,
prompt_persona.as_ref(),
&session,
&relevant_memories,
),
settings,
)
} else {
append_image_directive_instructions(
context.build_system_prompt(&character, &model, persona.as_ref(), &session),
context.build_system_prompt(
&character,
&model,
persona.as_ref(),
&session,
&relevant_memories,
),
settings,
)
};
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/chat_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pub use commands::{
chat_add_message_attachment, chat_completion, chat_continue, chat_generate_companion_soul,
chat_generate_design_reference_description, chat_generate_lorebook_entry_draft,
chat_generate_lorebook_keyword_draft, chat_generate_scene_image, chat_generate_scene_prompt,
chat_generate_user_reply, chat_message_debug_snapshot, chat_regenerate,
chat_generate_user_reply, chat_message_debug_snapshot, chat_regenerate, ChatMessageDebugSnapshot,
chat_template_export_as_usc, companion_clear_soul_growth, companion_remove_soul_growth,
companion_set_soul_growth_lock,
create_prompt_template, delete_prompt_template, dynamic_memory_cycle_status,
Expand Down
15 changes: 12 additions & 3 deletions src-tauri/src/chat_manager/persistence/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use crate::storage_manager::{
use crate::chat_manager::prompt_engine;
use crate::chat_manager::types::{
AccessibilitySettings, AccessibilitySoundSettings, AdvancedModelSettings, AdvancedSettings,
Character, DynamicMemoryStructuredFallbackFormat, Model, Persona, ProviderCredential, Session,
Settings, StoredMessage, SystemPromptEntry,
Character, DynamicMemoryStructuredFallbackFormat, MemoryEmbedding, Model, Persona,
ProviderCredential, Session, Settings, StoredMessage, SystemPromptEntry,
};

#[derive(Debug, Clone, Copy)]
Expand Down Expand Up @@ -528,8 +528,17 @@ pub fn build_system_prompt(
persona: Option<&Persona>,
session: &Session,
settings: &Settings,
retrieved_memories: &[MemoryEmbedding],
) -> Vec<SystemPromptEntry> {
prompt_engine::build_system_prompt_entries(app, character, model, persona, session, settings)
prompt_engine::build_system_prompt_entries(
app,
character,
model,
persona,
session,
settings,
retrieved_memories,
)
}

pub fn recent_messages(session: &Session, limit: usize) -> Vec<StoredMessage> {
Expand Down
Loading
Loading