Skip to content

Commit 5625410

Browse files
committed
refactor(chat): clean up various /compact issues
1 parent d13b4c1 commit 5625410

File tree

4 files changed

+109
-57
lines changed

4 files changed

+109
-57
lines changed

crates/q_cli/src/cli/chat/conversation_state.rs

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::collections::{
55
use std::env;
66
use std::sync::Arc;
77

8+
use aws_smithy_types::Document;
89
use fig_api_client::model::{
910
AssistantResponseMessage,
1011
ChatMessage,
@@ -32,11 +33,11 @@ use tracing::{
3233
warn,
3334
};
3435

35-
use super::chat_state::{
36+
use super::context::ContextManager;
37+
use super::summarization_state::{
3638
MAX_CHARS,
3739
TokenWarningLevel,
3840
};
39-
use super::context::ContextManager;
4041
use super::tools::{
4142
QueuedTool,
4243
ToolSpec,
@@ -123,14 +124,8 @@ impl ConversationState {
123124
}
124125
}
125126

126-
/// Returns a vector representation of the history (for accessors)
127-
pub fn history_as_vec(&self) -> Vec<ChatMessage> {
128-
self.history.iter().cloned().collect()
129-
}
130-
131-
/// Returns the length of the conversation history
132-
pub fn history_len(&self) -> usize {
133-
self.history.len()
127+
pub fn history(&self) -> &VecDeque<ChatMessage> {
128+
&self.history
134129
}
135130

136131
/// Clears the conversation history and optionally the summary.
@@ -476,10 +471,7 @@ impl ConversationState {
476471

477472
/// Calculate the total character count in the conversation
478473
pub fn calculate_char_count(&self) -> usize {
479-
// Calculate total character count in all messages
480474
let mut total_chars = 0;
481-
482-
// Count characters in history
483475
for message in &self.history {
484476
match message {
485477
ChatMessage::UserInputMessage(msg) => {
@@ -490,14 +482,11 @@ impl ConversationState {
490482
for result in results {
491483
for content in &result.content {
492484
match content {
493-
ToolResultContentBlock::Text(text) => total_chars += text.len(),
485+
ToolResultContentBlock::Text(text) => {
486+
total_chars += text.len();
487+
},
494488
ToolResultContentBlock::Json(doc) => {
495-
if let Some(s) = doc.as_string() {
496-
total_chars += s.len();
497-
} else {
498-
// Approximate JSON size
499-
total_chars += 100;
500-
}
489+
total_chars += calculate_document_char_count(doc);
501490
},
502491
}
503492
}
@@ -507,10 +496,12 @@ impl ConversationState {
507496
},
508497
ChatMessage::AssistantResponseMessage(msg) => {
509498
total_chars += msg.content.len();
510-
// Add tool uses if any
511499
if let Some(tool_uses) = &msg.tool_uses {
512-
// Approximation for tool uses
513-
total_chars += tool_uses.len() * 200;
500+
total_chars += tool_uses
501+
.iter()
502+
.map(|v| calculate_document_char_count(&v.input))
503+
.reduce(|acc, e| acc + e)
504+
.unwrap_or_default();
514505
}
515506
},
516507
}
@@ -607,8 +598,22 @@ fn build_shell_state() -> ShellState {
607598
}
608599
}
609600

601+
fn calculate_document_char_count(document: &Document) -> usize {
602+
match document {
603+
Document::Object(hash_map) => hash_map
604+
.values()
605+
.fold(0, |acc, e| acc + calculate_document_char_count(e)),
606+
Document::Array(vec) => vec.iter().fold(0, |acc, e| acc + calculate_document_char_count(e)),
607+
Document::Number(_) => 1,
608+
Document::String(s) => s.len(),
609+
Document::Bool(_) => 1,
610+
Document::Null => 1,
611+
}
612+
}
613+
610614
#[cfg(test)]
611615
mod tests {
616+
use aws_smithy_types::Number;
612617
use fig_api_client::model::{
613618
AssistantResponseMessage,
614619
ToolResultStatus,
@@ -635,6 +640,50 @@ mod tests {
635640
println!("{env_state:?}");
636641
}
637642

643+
#[test]
644+
fn test_calculate_document_char_count() {
645+
// Test simple types
646+
assert_eq!(calculate_document_char_count(&Document::String("hello".to_string())), 5);
647+
assert_eq!(calculate_document_char_count(&Document::Number(Number::PosInt(123))), 1);
648+
assert_eq!(calculate_document_char_count(&Document::Bool(true)), 1);
649+
assert_eq!(calculate_document_char_count(&Document::Null), 1);
650+
651+
// Test array
652+
let array = Document::Array(vec![
653+
Document::String("test".to_string()),
654+
Document::Number(Number::PosInt(42)),
655+
Document::Bool(false),
656+
]);
657+
assert_eq!(calculate_document_char_count(&array), 6); // "test" (4) + Number (1) + Bool (1)
658+
659+
// Test object
660+
let mut obj = HashMap::new();
661+
obj.insert("key1".to_string(), Document::String("value1".to_string()));
662+
obj.insert("key2".to_string(), Document::Number(Number::PosInt(99)));
663+
let object = Document::Object(obj);
664+
assert_eq!(calculate_document_char_count(&object), 7); // "value1" (6) + Number (1)
665+
666+
// Test nested structure
667+
let mut nested_obj = HashMap::new();
668+
let mut inner_obj = HashMap::new();
669+
inner_obj.insert("inner_key".to_string(), Document::String("inner_value".to_string()));
670+
nested_obj.insert("outer_key".to_string(), Document::Object(inner_obj));
671+
nested_obj.insert(
672+
"array_key".to_string(),
673+
Document::Array(vec![
674+
Document::String("item1".to_string()),
675+
Document::String("item2".to_string()),
676+
]),
677+
);
678+
679+
let complex = Document::Object(nested_obj);
680+
assert_eq!(calculate_document_char_count(&complex), 21); // "inner_value" (11) + "item1" (5) + "item2" (5)
681+
682+
// Test empty structures
683+
assert_eq!(calculate_document_char_count(&Document::Array(vec![])), 0);
684+
assert_eq!(calculate_document_char_count(&Document::Object(HashMap::new())), 0);
685+
}
686+
638687
fn assert_conversation_state_invariants(state: FigConversationState, i: usize) {
639688
if let Some(Some(msg)) = state.history.as_ref().map(|h| h.first()) {
640689
assert!(

crates/q_cli/src/cli/chat/mod.rs

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
mod chat_state;
21
mod command;
32
mod context;
43
mod conversation_state;
54
mod input_source;
65
mod parse;
76
mod parser;
87
mod prompt;
8+
mod summarization_state;
99
mod tools;
1010

11-
// Re-export types for use in this module
1211
use std::borrow::Cow;
1312
use std::collections::{
1413
HashMap,
1514
HashSet,
16-
VecDeque,
1715
};
1816
use std::io::{
1917
IsTerminal,
@@ -31,10 +29,6 @@ use std::{
3129
fs,
3230
};
3331

34-
use chat_state::{
35-
SummarizationState,
36-
TokenWarningLevel,
37-
};
3832
use command::{
3933
Command,
4034
ToolsSubcommand,
@@ -71,6 +65,10 @@ use fig_api_client::model::{
7165
use fig_os_shim::Context;
7266
use fig_settings::Settings;
7367
use fig_util::CLI_BINARY_NAME;
68+
use summarization_state::{
69+
SummarizationState,
70+
TokenWarningLevel,
71+
};
7472

7573
/// Help text for the compact command
7674
fn compact_help_text() -> String {
@@ -79,11 +77,11 @@ fn compact_help_text() -> String {
7977
<magenta,em>Conversation Compaction</magenta,em>
8078
8179
The <em>/compact</em> command summarizes the conversation history to free up context space
82-
while preserving the essential information. This is useful for long-running conversations
80+
while preserving essential information. This is useful for long-running conversations
8381
that may eventually reach memory constraints.
8482
8583
<cyan!>Usage</cyan!>
86-
<em>/compact</em> <black!>Summarize conversation and clear history</black!>
84+
<em>/compact</em> <black!>Summarize the conversation and clear history</black!>
8785
<em>/compact [prompt]</em> <black!>Provide custom guidance for summarization</black!>
8886
<em>/compact --summary</em> <black!>Show the summary after compacting</black!>
8987
@@ -157,7 +155,7 @@ const WELCOME_TEXT: &str = color_print::cstr! {"
157155
<em>/issue</em> <black!>Report an issue or make a feature request</black!>
158156
<em>/profile</em> <black!>(Beta) Manage profiles for the chat session</black!>
159157
<em>/context</em> <black!>(Beta) Manage context files for a profile</black!>
160-
<em>/compact</em> <black!>Summarize conversation to free up context space</black!>
158+
<em>/compact</em> <black!>Summarize the conversation to free up context space</black!>
161159
<em>/help</em> <black!>Show the help dialogue</black!>
162160
<em>/quit</em> <black!>Quit the application</black!>
163161
@@ -175,7 +173,7 @@ const HELP_TEXT: &str = color_print::cstr! {"
175173
<em>/editor</em> <black!>Open $EDITOR (defaults to vi) to compose a prompt</black!>
176174
<em>/help</em> <black!>Show this help dialogue</black!>
177175
<em>/quit</em> <black!>Quit the application</black!>
178-
<em>/compact</em> <black!>Summarize conversation to free up context space</black!>
176+
<em>/compact</em> <black!>Summarize the conversation to free up context space</black!>
179177
<em>help</em> <black!>Show help for the compact command</black!>
180178
<em>[prompt]</em> <black!>Optional custom prompt to guide summarization</black!>
181179
<em>--summary</em> <black!>Display the summary after compacting</black!>
@@ -858,7 +856,7 @@ where
858856
execute!(
859857
self.output,
860858
style::SetForegroundColor(Color::Green),
861-
style::Print("\nConversation history and summary cleared.\n\n"),
859+
style::Print("\nConversation history cleared.\n\n"),
862860
style::SetForegroundColor(Color::Reset)
863861
)?;
864862

@@ -890,7 +888,7 @@ where
890888
}
891889

892890
// Check if conversation history is long enough to compact
893-
if self.conversation_state.history_len() <= 3 {
891+
if self.conversation_state.history().len() <= 3 {
894892
execute!(
895893
self.output,
896894
style::SetForegroundColor(Color::Yellow),
@@ -905,23 +903,12 @@ where
905903
});
906904
}
907905

908-
// Get private history field using accessor
909-
let saved_history = VecDeque::from(self.conversation_state.history_as_vec().clone());
910-
911906
// Set up summarization state with history, custom prompt, and show_summary flag
912907
let mut summarization_state = SummarizationState::with_prompt(prompt.clone());
913-
summarization_state.original_history = Some(saved_history);
908+
summarization_state.original_history = Some(self.conversation_state.history().clone());
914909
summarization_state.show_summary = show_summary; // Store the show_summary flag
915910
self.summarization_state = Some(summarization_state);
916911

917-
// Tell user we're working on the summary
918-
execute!(
919-
self.output,
920-
style::SetForegroundColor(Color::Green),
921-
style::Print("\nCompacting conversation history...\n"),
922-
style::SetForegroundColor(Color::Reset)
923-
)?;
924-
925912
// Create a summary request based on user input or default
926913
let summary_request = match prompt {
927914
Some(custom_prompt) => {
@@ -971,7 +958,7 @@ where
971958

972959
// Use spinner while we wait
973960
if self.interactive {
974-
queue!(self.output, cursor::Hide)?;
961+
execute!(self.output, cursor::Hide, style::Print("\n"))?;
975962
self.spinner = Some(Spinner::new(Spinners::Dots, "Creating summary...".to_string()));
976963
}
977964

@@ -1821,7 +1808,13 @@ where
18211808
buf.push('\n');
18221809
}
18231810

1824-
if tool_name_being_recvd.is_none() && !buf.is_empty() && self.interactive && self.spinner.is_some() {
1811+
// TODO: refactor summarization into a separate ChatState value
1812+
if tool_name_being_recvd.is_none()
1813+
&& !buf.is_empty()
1814+
&& self.interactive
1815+
&& self.spinner.is_some()
1816+
&& !is_summarization
1817+
{
18251818
drop(self.spinner.take());
18261819
queue!(
18271820
self.output,
@@ -1905,8 +1898,18 @@ where
19051898

19061899
// Handle summarization completion if we were in summarization mode
19071900
if let Some(summarization_state) = self.summarization_state.take() {
1901+
if self.spinner.is_some() {
1902+
drop(self.spinner.take());
1903+
queue!(
1904+
self.output,
1905+
terminal::Clear(terminal::ClearType::CurrentLine),
1906+
cursor::MoveToColumn(0),
1907+
cursor::Show
1908+
)?;
1909+
}
1910+
19081911
// Get the latest message content (the summary)
1909-
let summary = match self.conversation_state.history_as_vec().last() {
1912+
let summary = match self.conversation_state.history().back() {
19101913
Some(ChatMessage::AssistantResponseMessage(message)) => message.content.clone(),
19111914
_ => "Summary could not be generated.".to_string(),
19121915
};
@@ -1928,12 +1931,10 @@ where
19281931
// Add the message
19291932
self.conversation_state.push_assistant_message(special_message);
19301933

1931-
// Display confirmation message
1932-
19331934
execute!(
19341935
self.output,
19351936
style::SetForegroundColor(Color::Green),
1936-
style::Print("\n Conversation has been successfully summarized and cleared!\n\n"),
1937+
style::Print(" Conversation history has been compacted successfully!\n\n"),
19371938
style::SetForegroundColor(Color::DarkGrey)
19381939
)?;
19391940

@@ -1969,18 +1970,17 @@ where
19691970
style::Print("\n"),
19701971
style::SetAttribute(Attribute::Bold),
19711972
style::Print(" CONVERSATION SUMMARY"),
1972-
style::SetAttribute(Attribute::Reset),
19731973
style::Print("\n"),
19741974
style::Print(&border),
1975-
style::SetForegroundColor(Color::Reset),
1975+
style::SetAttribute(Attribute::Reset),
19761976
style::Print("\n\n"),
19771977
style::Print(&summary),
19781978
style::Print("\n\n"),
19791979
style::SetForegroundColor(Color::Cyan),
19801980
style::Print("This summary is stored in memory and available to the assistant.\n"),
19811981
style::Print("It contains all important details from previous interactions.\n"),
19821982
style::Print(&border),
1983-
style::Print("\n"),
1983+
style::Print("\n\n"),
19841984
style::SetForegroundColor(Color::Reset)
19851985
)?;
19861986
}

crates/q_cli/src/cli/chat/prompt.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ const COMMANDS: &[&str] = &[
6464
"/context rm --global",
6565
"/context clear",
6666
"/context clear --global",
67+
"/compact",
68+
"/compact help",
69+
"/compact --summary",
6770
];
6871

6972
pub fn generate_prompt(current_profile: Option<&str>, warning: bool) -> String {

0 commit comments

Comments
 (0)