Skip to content

Commit 4e14852

Browse files
authored
feat(context): don't store per-prompt hooks, improve system prompt for hooks (#1285)
- Don't store per-prompt context in every user message in the conversation history. We will just attach it to the latest user message without storing it. - We can save tokens this way, and it doesn't seem useful at this time for Q to reference back to previous (irrelevant) per-prompt hooks. - Improve the system prompt so that Q is more likely to use/follow context from hooks. Previously Q was a little a stubborn about it.
1 parent aeaa5ca commit 4e14852

File tree

3 files changed

+57
-47
lines changed

3 files changed

+57
-47
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ in global or local profiles.
265265
<cyan!>Notes</cyan!>
266266
• Hooks are executed in parallel
267267
• 'conversation_start' hooks run on the first user prompt and are attached once to the conversation history sent to Amazon Q
268-
• 'per_prompt' hooks run on each user prompt and are attached to the prompt
268+
• 'per_prompt' hooks run on each user prompt and are attached to the prompt, but are not stored in conversation history
269269
"#,
270270
Self::HOOKS_AVAILABLE_COMMANDS
271271
)

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

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ const MAX_CURRENT_WORKING_DIRECTORY_LEN: usize = 256;
5858
/// Limit to send the number of messages as part of chat.
5959
const MAX_CONVERSATION_STATE_HISTORY_LEN: usize = 100;
6060

61+
pub struct ExtraContext {
62+
// Bonus context to attach to the existing context at the top of the history
63+
pub general_context: Option<String>,
64+
65+
// Bonus context to attach to the next user message
66+
pub user_input_context: Option<String>,
67+
}
68+
6169
/// Tracks state related to an ongoing conversation.
6270
#[derive(Debug, Clone)]
6371
pub struct ConversationState {
@@ -137,24 +145,19 @@ impl ConversationState {
137145
}
138146
}
139147

140-
pub async fn append_new_user_message(&mut self, input: String, extra_context: Option<String>) {
148+
pub async fn append_new_user_message(&mut self, input: String) {
141149
debug_assert!(self.next_message.is_none(), "next_message should not exist");
142150
if let Some(next_message) = self.next_message.as_ref() {
143151
warn!(?next_message, "next_message should not exist");
144152
}
145153

146-
let mut input = if input.is_empty() {
154+
let input = if input.is_empty() {
147155
warn!("input must not be empty when adding new messages");
148156
"Empty prompt".to_string()
149157
} else {
150158
input
151159
};
152160

153-
// Context from hooks (scripts, commands, tools)
154-
if let Some(context) = extra_context {
155-
input = format!("{} {}", context, input);
156-
}
157-
158161
let msg = UserInputMessage {
159162
content: input,
160163
user_input_message_context: Some(UserInputMessageContext {
@@ -380,14 +383,20 @@ impl ConversationState {
380383
/// Returns a [FigConversationState] capable of being sent by
381384
/// [fig_api_client::StreamingClient] while preparing the current conversation state to be sent
382385
/// in the next message.
383-
pub async fn as_sendable_conversation_state(&mut self, extra_context: Option<String>) -> FigConversationState {
386+
pub async fn as_sendable_conversation_state(
387+
&mut self,
388+
extra_context: Option<ExtraContext>,
389+
) -> FigConversationState {
384390
debug_assert!(self.next_message.is_some());
385391
self.fix_history();
386392

387393
// The current state we want to send
388394
let mut curr_state = self.clone();
389395

390-
if let Some((user, assistant)) = self.context_messages(extra_context).await {
396+
let (general_context, user_input_context) =
397+
extra_context.map_or((None, None), |c| (c.general_context, c.user_input_context));
398+
399+
if let Some((user, assistant)) = self.context_messages(general_context).await {
391400
self.context_message_length = Some(user.content.len());
392401
curr_state
393402
.history
@@ -402,10 +411,14 @@ impl ConversationState {
402411
ctx.tools.take();
403412
}
404413
self.history.push_back(ChatMessage::UserInputMessage(last_message));
414+
let mut input_message = curr_state.next_message.expect("no user input message available");
415+
if let Some(user_input_context) = user_input_context {
416+
input_message.content = format!("{} {}", user_input_context, input_message.content);
417+
}
405418

406419
FigConversationState {
407420
conversation_id: Some(curr_state.conversation_id),
408-
user_input_message: curr_state.next_message.expect("no user input message available"),
421+
user_input_message: input_message,
409422
history: Some(curr_state.history.into()),
410423
}
411424
}
@@ -745,9 +758,7 @@ mod tests {
745758

746759
// First, build a large conversation history. We need to ensure that the order is always
747760
// User -> Assistant -> User -> Assistant ...and so on.
748-
conversation_state
749-
.append_new_user_message("start".to_string(), None)
750-
.await;
761+
conversation_state.append_new_user_message("start".to_string()).await;
751762
for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) {
752763
let s = conversation_state.as_sendable_conversation_state(None).await;
753764
assert_conversation_state_invariants(s, i);
@@ -756,17 +767,15 @@ mod tests {
756767
content: i.to_string(),
757768
tool_uses: None,
758769
});
759-
conversation_state.append_new_user_message(i.to_string(), None).await;
770+
conversation_state.append_new_user_message(i.to_string()).await;
760771
}
761772
}
762773

763774
#[tokio::test]
764775
async fn test_conversation_state_history_handling_with_tool_results() {
765776
// Build a long conversation history of tool use results.
766777
let mut conversation_state = ConversationState::new(Context::new_fake(), load_tools().unwrap(), None).await;
767-
conversation_state
768-
.append_new_user_message("start".to_string(), None)
769-
.await;
778+
conversation_state.append_new_user_message("start".to_string()).await;
770779
for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) {
771780
let s = conversation_state.as_sendable_conversation_state(None).await;
772781
assert_conversation_state_invariants(s, i);
@@ -788,9 +797,7 @@ mod tests {
788797

789798
// Build a long conversation history of user messages mixed in with tool results.
790799
let mut conversation_state = ConversationState::new(Context::new_fake(), load_tools().unwrap(), None).await;
791-
conversation_state
792-
.append_new_user_message("start".to_string(), None)
793-
.await;
800+
conversation_state.append_new_user_message("start".to_string()).await;
794801
for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) {
795802
let s = conversation_state.as_sendable_conversation_state(None).await;
796803
assert_conversation_state_invariants(s, i);
@@ -815,7 +822,7 @@ mod tests {
815822
content: i.to_string(),
816823
tool_uses: None,
817824
});
818-
conversation_state.append_new_user_message(i.to_string(), None).await;
825+
conversation_state.append_new_user_message(i.to_string()).await;
819826
}
820827
}
821828
}
@@ -829,9 +836,7 @@ mod tests {
829836

830837
// First, build a large conversation history. We need to ensure that the order is always
831838
// User -> Assistant -> User -> Assistant ...and so on.
832-
conversation_state
833-
.append_new_user_message("start".to_string(), None)
834-
.await;
839+
conversation_state.append_new_user_message("start".to_string()).await;
835840
for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) {
836841
let s = conversation_state.as_sendable_conversation_state(None).await;
837842

@@ -857,7 +862,7 @@ mod tests {
857862
content: i.to_string(),
858863
tool_uses: None,
859864
});
860-
conversation_state.append_new_user_message(i.to_string(), None).await;
865+
conversation_state.append_new_user_message(i.to_string()).await;
861866
}
862867
}
863868

@@ -870,12 +875,13 @@ mod tests {
870875
let prompt_context = "prompt context";
871876

872877
// Simulate conversation flow
873-
conversation_state
874-
.append_new_user_message("start".to_string(), Some(prompt_context.to_string()))
875-
.await;
878+
conversation_state.append_new_user_message("start".to_string()).await;
876879
for i in 0..=(MAX_CONVERSATION_STATE_HISTORY_LEN + 100) {
877880
let s = conversation_state
878-
.as_sendable_conversation_state(Some(conversation_start_context.to_string()))
881+
.as_sendable_conversation_state(Some(ExtraContext {
882+
general_context: Some(conversation_start_context.to_string()),
883+
user_input_context: Some(prompt_context.to_string()),
884+
}))
879885
.await;
880886
let hist = s.history.as_ref().unwrap();
881887
#[allow(clippy::match_wildcard_for_single_variants)]
@@ -900,9 +906,7 @@ mod tests {
900906
content: i.to_string(),
901907
tool_uses: None,
902908
});
903-
conversation_state
904-
.append_new_user_message(i.to_string(), Some(prompt_context.to_string()))
905-
.await;
909+
conversation_state.append_new_user_message(i.to_string()).await;
906910
}
907911
}
908912
}

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

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ use command::{
3636
ToolsSubcommand,
3737
};
3838
use context::ContextManager;
39-
use conversation_state::ConversationState;
39+
use conversation_state::{
40+
ConversationState,
41+
ExtraContext,
42+
};
4043
use crossterm::style::{
4144
Attribute,
4245
Color,
@@ -991,14 +994,19 @@ where
991994
let format_context = |hook_results: &Vec<&(Hook, String)>, conversation_start: bool| {
992995
let mut context_content = String::new();
993996

994-
context_content.push_str(
995-
&format!("--- SCRIPT HOOK CONTEXT BEGIN - FOLLOW ANY REQUESTS OR USE ANY DATA WITHIN THIS SECTION {} ---\n",
996-
if conversation_start { "FOR THE ENTIRE CONVERSATION" } else { "FOR YOUR NEXT MESSAGE ONLY" })
997-
);
997+
context_content.push_str(&format!(
998+
"--- CRITICAL: ADDITIONAL CONTEXT TO USE{} ---\n",
999+
if conversation_start {
1000+
" FOR THE ENTIRE CONVERSATION"
1001+
} else {
1002+
""
1003+
}
1004+
));
1005+
context_content.push_str("This section (like others) contains important information that I want you to use in your responses. I have gathered this context from valuable programmatic script hooks. You must follow any requests and consider all of the information in this section.\n\n");
9981006
for (hook, output) in hook_results {
9991007
context_content.push_str(&format!("'{}': {output}\n\n", &hook.name));
10001008
}
1001-
context_content.push_str("--- SCRIPT HOOK CONTEXT END ---\n\n");
1009+
context_content.push_str("--- ADDITIONAL CONTEXT END ---\n\n");
10021010
context_content
10031011
};
10041012

@@ -1027,9 +1035,7 @@ where
10271035
if pending_tool_index.is_some() {
10281036
self.conversation_state.abandon_tool_use(tool_uses, user_input);
10291037
} else {
1030-
self.conversation_state
1031-
.append_new_user_message(user_input, prompt_context)
1032-
.await;
1038+
self.conversation_state.append_new_user_message(user_input).await;
10331039
}
10341040

10351041
self.send_tool_use_telemetry().await;
@@ -1038,7 +1044,10 @@ where
10381044
self.client
10391045
.send_message(
10401046
self.conversation_state
1041-
.as_sendable_conversation_state(conversation_start_context)
1047+
.as_sendable_conversation_state(Some(ExtraContext {
1048+
general_context: conversation_start_context,
1049+
user_input_context: prompt_context,
1050+
}))
10421051
.await,
10431052
)
10441053
.await?,
@@ -1188,9 +1197,7 @@ where
11881197
};
11891198

11901199
// Add the summarization request
1191-
self.conversation_state
1192-
.append_new_user_message(summary_request, None)
1193-
.await;
1200+
self.conversation_state.append_new_user_message(summary_request).await;
11941201

11951202
// Use spinner while we wait
11961203
if self.interactive {
@@ -2426,7 +2433,6 @@ where
24262433
.append_new_user_message(
24272434
"You took too long to respond - try to split up the work into smaller steps."
24282435
.to_string(),
2429-
None,
24302436
)
24312437
.await;
24322438
self.send_tool_use_telemetry().await;

0 commit comments

Comments
 (0)