Skip to content

Commit 46152f2

Browse files
committed
feat: enhance proposal generation and synthesis handling; remove revision functionality
1 parent 479d40e commit 46152f2

File tree

2 files changed

+122
-106
lines changed

2 files changed

+122
-106
lines changed

crates/agentic-core/src/orchestrator.rs

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -28,31 +28,26 @@ You MUST generate EXACTLY 3 proposals about this query: "{query}"
2828
}
2929
"#;
3030

31-
const REVISE_PROMPT: &str = r#"You are an expert prompt engineer. A user wants to revise a prompt proposal.
3231

33-
Original Proposal: "{proposal}"
34-
User's Revision: "{revision}"
35-
36-
Your task is to integrate the user's revision into the original proposal to create a new, single, improved prompt.
37-
The new prompt should be self-contained and ready to use.
38-
39-
Format your response as a JSON object with a single key "proposal" which is a string.
40-
Example:
41-
{
42-
"proposal": "This is the new, revised prompt."
32+
#[derive(Deserialize, Debug)]
33+
struct ProposalObject {
34+
context: String,
35+
question: String,
4336
}
44-
"#;
4537

4638
#[derive(Deserialize, Debug)]
47-
struct ProposalsResponse {
48-
proposals: Vec<String>,
39+
#[serde(untagged)]
40+
enum ProposalItem {
41+
StringFormat(String),
42+
ObjectFormat(ProposalObject),
4943
}
5044

5145
#[derive(Deserialize, Debug)]
52-
struct ReviseResponse {
53-
proposal: String,
46+
struct ProposalsResponse {
47+
proposals: Vec<ProposalItem>,
5448
}
5549

50+
5651
pub async fn generate_proposals(
5752
query: &str,
5853
endpoint: &str,
@@ -72,7 +67,19 @@ pub async fn generate_proposals(
7267
if let Some(json_start) = response_str.find("{") {
7368
let json_str = &response_str[json_start..];
7469
match serde_json::from_str::<ProposalsResponse>(json_str) {
75-
Ok(response) => Ok(response.proposals),
70+
Ok(response) => {
71+
let proposals = response
72+
.proposals
73+
.into_iter()
74+
.map(|item| match item {
75+
ProposalItem::StringFormat(s) => s,
76+
ProposalItem::ObjectFormat(obj) => {
77+
format!("{} - {}", obj.context, obj.question)
78+
}
79+
})
80+
.collect();
81+
Ok(proposals)
82+
}
7683
Err(e) => {
7784
// Debug: Write the JSON we tried to parse
7885
std::fs::write("/tmp/debug_json.txt", json_str).ok();
@@ -91,25 +98,3 @@ pub async fn generate_proposals(
9198
}
9299
}
93100

94-
pub async fn revise_proposal(
95-
proposal: &str,
96-
revision: &str,
97-
endpoint: &str,
98-
model: &str,
99-
) -> Result<String, anyhow::Error> {
100-
let prompt = REVISE_PROMPT
101-
.replace("{proposal}", proposal)
102-
.replace("{revision}", revision);
103-
let response_str = call_local_model(endpoint, model, &prompt).await?;
104-
105-
// Attempt to find the start of the JSON object
106-
if let Some(json_start) = response_str.find("{") {
107-
let json_str = &response_str[json_start..];
108-
match serde_json::from_str::<ReviseResponse>(json_str) {
109-
Ok(response) => Ok(response.proposal),
110-
Err(e) => Err(anyhow::anyhow!("Failed to parse revision JSON: {}", e)),
111-
}
112-
} else {
113-
Err(anyhow::anyhow!("No JSON object found in model response"))
114-
}
115-
}

crates/agentic-tui/src/ui/app.rs

Lines changed: 98 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ pub enum AppMode {
3232
SelectingLocalModel,
3333
SelectingCloudModel,
3434
Orchestrating,
35-
Revising,
3635
Complete,
3736
CoachingTip,
3837
// TODO: Add About mode
@@ -65,7 +64,6 @@ pub enum ValidationMessage {
6564
#[derive(Debug)]
6665
pub enum AgentMessage {
6766
ProposalsGenerated(Result<Vec<String>, anyhow::Error>),
68-
RevisedProposalGenerated(Result<String, anyhow::Error>),
6967
CloudSynthesisComplete(Result<AtomicNote, CloudError>),
7068
}
7169

@@ -122,6 +120,7 @@ pub struct App {
122120
models_per_page: usize,
123121
proposals: Vec<String>,
124122
current_proposal_index: usize,
123+
original_user_query: String, // Store the user's original query for metadata
125124
final_prompt: String,
126125
cloud_response: Option<AtomicNote>,
127126
synthesis_scroll: u16,
@@ -150,6 +149,7 @@ impl App {
150149
models_per_page: 10, // Show 10 models per page
151150
proposals: Vec::new(),
152151
current_proposal_index: 0,
152+
original_user_query: String::new(),
153153
final_prompt: String::new(),
154154
cloud_response: None,
155155
synthesis_scroll: 0,
@@ -238,8 +238,11 @@ impl App {
238238

239239
frame.render_widget(proposals_paragraph, chunks[1]);
240240

241-
// Footer with controls
242-
let footer_text = "[Enter] Synthesize | [E]dit Selected | [ESC] Cancel";
241+
// Footer with controls - dynamic based on synthesis status
242+
let footer_text = match self.agent_status {
243+
AgentStatus::Searching => "⏳ Synthesizing... | [ESC] Cancel",
244+
_ => "[Enter] Synthesize | [ESC] Cancel",
245+
};
243246
let footer = Paragraph::new(footer_text)
244247
.alignment(Alignment::Center)
245248
.style(self.theme.ratatui_style(Element::Inactive));
@@ -457,16 +460,31 @@ impl App {
457460
frame.render_widget(Clear, modal_area);
458461
self.render_coaching_tip_modal(frame, modal_area);
459462
} else if self.mode == AppMode::Complete {
463+
// Center the synthesis content for better visual balance
460464
let content = if let Some(note) = &self.cloud_response {
461465
// Clean display - only show the synthesis content, hide system metadata
462466
Paragraph::new(note.body_text.as_str())
463467
.style(self.theme.ratatui_style(Element::Text))
468+
.alignment(ratatui::prelude::Alignment::Center)
464469
} else {
465470
// This case should ideally not be reached if mode is Complete
466471
Paragraph::new("Waiting for synthesis...")
467472
.style(self.theme.ratatui_style(Element::Text))
473+
.alignment(ratatui::prelude::Alignment::Center)
468474
};
469475

476+
// Create a centered area for the synthesis (70% width, centered vertically)
477+
let main_area = app_chunks[1];
478+
let synthesis_width = (main_area.width * 70 / 100).max(40).min(main_area.width);
479+
let synthesis_height = 10; // Fixed height for the synthesis box
480+
481+
let synthesis_area = Rect::new(
482+
main_area.x + (main_area.width.saturating_sub(synthesis_width)) / 2,
483+
main_area.y + (main_area.height.saturating_sub(synthesis_height)) / 2,
484+
synthesis_width,
485+
synthesis_height,
486+
);
487+
470488
let block = Block::default()
471489
.title(" Synthesis Complete ")
472490
.borders(Borders::ALL)
@@ -477,7 +495,7 @@ impl App {
477495
.wrap(Wrap { trim: true })
478496
.scroll((self.synthesis_scroll, 0));
479497

480-
frame.render_widget(paragraph, app_chunks[1]);
498+
frame.render_widget(paragraph, synthesis_area);
481499
} else {
482500
render_chat(
483501
frame,
@@ -604,19 +622,6 @@ impl App {
604622
self.mode = AppMode::CoachingTip;
605623
self.agent_status = AgentStatus::Ready;
606624
}
607-
AgentMessage::RevisedProposalGenerated(Ok(proposal)) => {
608-
self.proposals[self.current_proposal_index] = proposal;
609-
self.mode = AppMode::Orchestrating;
610-
self.agent_status = AgentStatus::Ready;
611-
}
612-
AgentMessage::RevisedProposalGenerated(Err(_e)) => {
613-
self.coaching_tip = (
614-
"Local Model Error".to_string(),
615-
"The local model failed to revise the proposal. Check if it is running and configured correctly.".to_string(),
616-
);
617-
self.mode = AppMode::CoachingTip;
618-
self.agent_status = AgentStatus::Ready;
619-
}
620625
AgentMessage::CloudSynthesisComplete(Ok(response)) => {
621626
self.cloud_response = Some(response);
622627
self.mode = AppMode::Complete;
@@ -883,39 +888,22 @@ impl App {
883888
}
884889
KeyCode::Enter => {
885890
// Synthesize - send proposal to cloud for synthesis
886-
if let Some(proposal) =
887-
self.proposals.get(self.current_proposal_index)
888-
{
889-
self.final_prompt = proposal.clone();
890-
self.handle_cloud_synthesis();
891+
// Rate limiting: only allow if not already processing
892+
if self.agent_status != AgentStatus::Searching {
893+
if let Some(proposal) =
894+
self.proposals.get(self.current_proposal_index)
895+
{
896+
self.final_prompt = proposal.clone();
897+
self.handle_cloud_synthesis();
898+
}
891899
}
892900
}
893-
KeyCode::Char('e') => {
894-
// Edit selected proposal
895-
self.mode = AppMode::Revising;
896-
self.edit_buffer.clear();
897-
}
898901
KeyCode::Esc => {
899902
// Cancel and return to normal mode
900903
self.mode = AppMode::Normal;
901904
self.proposals.clear();
902905
self.current_proposal_index = 0;
903-
}
904-
_ => {}
905-
},
906-
AppMode::Revising => match key.code {
907-
KeyCode::Enter => {
908-
self.handle_revision();
909-
}
910-
KeyCode::Esc => {
911-
self.mode = AppMode::Orchestrating;
912-
self.edit_buffer.clear();
913-
}
914-
KeyCode::Backspace => {
915-
self.edit_buffer.pop();
916-
}
917-
KeyCode::Char(c) => {
918-
self.edit_buffer.push(c);
906+
self.original_user_query.clear();
919907
}
920908
_ => {}
921909
},
@@ -927,6 +915,7 @@ impl App {
927915
self.final_prompt.clear();
928916
self.proposals.clear();
929917
self.current_proposal_index = 0;
918+
self.original_user_query.clear();
930919
self.cloud_response = None;
931920
self.synthesis_scroll = 0;
932921
self.agent_status = AgentStatus::Ready;
@@ -937,6 +926,7 @@ impl App {
937926
self.final_prompt.clear();
938927
self.proposals.clear();
939928
self.current_proposal_index = 0;
929+
self.original_user_query.clear();
940930
self.cloud_response = None;
941931
self.synthesis_scroll = 0;
942932
self.agent_status = AgentStatus::Ready;
@@ -980,6 +970,9 @@ impl App {
980970
self.handle_slash_command(&message);
981971
} else {
982972
// Handle regular chat message
973+
// Store the original user query for metadata
974+
self.original_user_query = message.clone();
975+
983976
self.agent_status = AgentStatus::Orchestrating;
984977
let settings = self.settings.clone();
985978
let tx = self.agent_tx.clone();
@@ -998,31 +991,18 @@ impl App {
998991
self.edit_buffer.clear();
999992
}
1000993

1001-
fn handle_revision(&mut self) {
1002-
let revision = self.edit_buffer.trim().to_string();
1003-
if let Some(current_proposal) = self.proposals.get(self.current_proposal_index).cloned() {
1004-
self.agent_status = AgentStatus::Orchestrating;
1005-
let settings = self.settings.clone();
1006-
let tx = self.agent_tx.clone();
1007-
tokio::spawn(async move {
1008-
let result = orchestrator::revise_proposal(
1009-
&current_proposal,
1010-
&revision,
1011-
&settings.endpoint,
1012-
&settings.local_model,
1013-
)
1014-
.await;
1015-
let _ = tx.send(AgentMessage::RevisedProposalGenerated(result));
1016-
});
1017-
}
1018-
self.edit_buffer.clear();
1019-
}
1020994

1021995
fn save_synthesis(&self) {
1022996
if let Some(note) = &self.cloud_response {
1023-
// Generate markdown content with v0.1.0 metadata structure
1024-
let timestamp = chrono::Utc::now().format("%Y-%m-%d_%H-%M-%S").to_string();
1025-
let filename = format!("synthesis_{}.md", timestamp);
997+
// Generate meaningful filename from query and metadata
998+
let timestamp = chrono::Utc::now();
999+
let date_part = timestamp.format("%Y-%m-%d").to_string();
1000+
1001+
// Extract keywords from original user query for filename
1002+
let keywords = self.extract_filename_keywords(&self.original_user_query, &note.header_tags);
1003+
let time_suffix = timestamp.format("-%H%M").to_string(); // Add time for uniqueness
1004+
1005+
let filename = format!("{}-{}{}.md", date_part, keywords, time_suffix);
10261006

10271007
// Get the selected proposal text
10281008
let proposal_text = if !self.proposals.is_empty()
@@ -1044,7 +1024,7 @@ impl App {
10441024
} else {
10451025
&self.settings.cloud_model
10461026
},
1047-
self.final_prompt.replace("\"", "\\\""),
1027+
self.original_user_query.replace("\"", "\\\""),
10481028
clean_proposal.replace("\"", "\\\""),
10491029
note.header_tags.iter().map(|tag| format!("\"{}\"", tag)).collect::<Vec<_>>().join(", "),
10501030
note.header_tags.join(" • "),
@@ -1229,4 +1209,55 @@ impl App {
12291209
let target_page = self.selected_model_index / self.models_per_page;
12301210
self.current_page = target_page;
12311211
}
1212+
1213+
fn extract_filename_keywords(&self, query: &str, meta_tags: &[String]) -> String {
1214+
// Common words to filter out
1215+
let stop_words = [
1216+
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with",
1217+
"by", "is", "are", "was", "were", "be", "been", "have", "has", "had", "do", "does",
1218+
"did", "will", "would", "could", "should", "can", "what", "where", "when", "why",
1219+
"how", "who", "which", "that", "this", "these", "those", "i", "you", "he", "she",
1220+
"it", "we", "they", "me", "him", "her", "us", "them", "my", "your", "his", "her",
1221+
"its", "our", "their"
1222+
];
1223+
1224+
// Extract meaningful words from query
1225+
let query_words: Vec<String> = query
1226+
.to_lowercase()
1227+
.split_whitespace()
1228+
.filter_map(|word| {
1229+
// Clean up punctuation
1230+
let clean_word = word.trim_matches(|c: char| !c.is_alphanumeric());
1231+
1232+
// Filter out stop words and short words
1233+
if clean_word.len() >= 3 && !stop_words.contains(&clean_word) {
1234+
Some(clean_word.to_string())
1235+
} else {
1236+
None
1237+
}
1238+
})
1239+
.take(3) // Limit to 3 keywords from query
1240+
.collect();
1241+
1242+
// If we got good keywords from query, use them
1243+
if query_words.len() >= 2 {
1244+
query_words.join("-")
1245+
} else {
1246+
// Fallback to first meta tag if query didn't provide enough keywords
1247+
if let Some(first_tag) = meta_tags.first() {
1248+
first_tag
1249+
.to_lowercase()
1250+
.replace(' ', "-")
1251+
.chars()
1252+
.filter(|c| c.is_alphanumeric() || *c == '-')
1253+
.collect::<String>()
1254+
.trim_matches('-')
1255+
.to_string()
1256+
} else {
1257+
// Final fallback
1258+
"synthesis".to_string()
1259+
}
1260+
}
1261+
}
1262+
12321263
}

0 commit comments

Comments
 (0)