Skip to content

Commit cce59e5

Browse files
committed
feat: implement cloud synthesis functionality and enhance agent status handling
1 parent eaf5eee commit cce59e5

File tree

4 files changed

+145
-15
lines changed

4 files changed

+145
-15
lines changed

crates/agentic-core/src/cloud.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use reqwest::Client;
2+
use serde::{Deserialize, Serialize};
3+
use std::time::Duration;
4+
5+
#[derive(Serialize)]
6+
struct OpenRouterRequest {
7+
model: String,
8+
messages: Vec<ChatMessage>,
9+
max_tokens: u32,
10+
}
11+
12+
#[derive(Serialize)]
13+
struct ChatMessage {
14+
role: String,
15+
content: String,
16+
}
17+
18+
#[derive(Deserialize)]
19+
struct OpenRouterResponse {
20+
choices: Vec<Choice>,
21+
}
22+
23+
#[derive(Deserialize)]
24+
struct Choice {
25+
message: Message,
26+
}
27+
28+
#[derive(Deserialize)]
29+
struct Message {
30+
content: String,
31+
}
32+
33+
pub async fn call_cloud_model(
34+
api_key: &str,
35+
model: &str,
36+
prompt: &str,
37+
) -> Result<String, anyhow::Error> {
38+
let client = Client::builder().timeout(Duration::from_secs(30)).build()?;
39+
40+
// Optimize prompt for concise responses
41+
let optimized_prompt = format!(
42+
"Please provide a concise, well-structured response to this inquiry. Keep it informative but focused:\n\n{}",
43+
prompt
44+
);
45+
46+
let request_body = OpenRouterRequest {
47+
model: model.to_string(),
48+
messages: vec![ChatMessage {
49+
role: "user".to_string(),
50+
content: optimized_prompt,
51+
}],
52+
max_tokens: 1024, // Reduced from 2048 for more concise responses
53+
};
54+
55+
let response = client
56+
.post("https://openrouter.ai/api/v1/chat/completions")
57+
.header("Authorization", format!("Bearer {}", api_key))
58+
.header("Content-Type", "application/json")
59+
.json(&request_body)
60+
.send()
61+
.await?;
62+
63+
if !response.status().is_success() {
64+
let status = response.status();
65+
let error_text = response.text().await.unwrap_or_default();
66+
return Err(anyhow::anyhow!(
67+
"OpenRouter API error {}: {}",
68+
status,
69+
error_text
70+
));
71+
}
72+
73+
let openrouter_response: OpenRouterResponse = response.json().await?;
74+
75+
if let Some(choice) = openrouter_response.choices.first() {
76+
Ok(choice.message.content.clone())
77+
} else {
78+
Err(anyhow::anyhow!("No response choices from OpenRouter API"))
79+
}
80+
}

crates/agentic-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//! - `settings`: Application configuration management
1111
//! - `theme`: UI theming system
1212
13+
pub mod cloud;
1314
pub mod models;
1415
pub mod orchestrator;
1516
pub mod settings;

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

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use super::{
66
settings_modal::render_settings_modal,
77
};
88
use agentic_core::{
9+
cloud,
910
models::{ModelValidator, OllamaModel, OpenRouterModel},
1011
orchestrator,
1112
settings::{Settings, ValidationError},
@@ -49,6 +50,8 @@ pub enum AgentStatus {
4950
LocalEndpointError,
5051
CloudEndpointError,
5152
Orchestrating,
53+
Searching,
54+
Complete,
5255
}
5356

5457
#[derive(Debug)]
@@ -63,6 +66,7 @@ pub enum ValidationMessage {
6366
pub enum AgentMessage {
6467
ProposalsGenerated(Result<Vec<String>, anyhow::Error>),
6568
RevisedProposalGenerated(Result<String, anyhow::Error>),
69+
CloudSynthesisComplete(Result<String, anyhow::Error>),
6670
}
6771

6872
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -119,6 +123,8 @@ pub struct App {
119123
proposals: Vec<String>,
120124
current_proposal_index: usize,
121125
final_prompt: String,
126+
cloud_response: String,
127+
synthesis_scroll: u16,
122128
}
123129

124130
impl App {
@@ -144,6 +150,8 @@ impl App {
144150
proposals: Vec::new(),
145151
current_proposal_index: 0,
146152
final_prompt: String::new(),
153+
cloud_response: String::new(),
154+
synthesis_scroll: 0,
147155
}
148156
}
149157

@@ -202,7 +210,7 @@ impl App {
202210
// Clean up the proposal text - remove template artifacts
203211
let proposal_text = proposal
204212
.replace("Context statement: ", "")
205-
.replace("Another context: ", "")
213+
.replace("Another context: ", "")
206214
.replace("Third context: ", "")
207215
.replace("Context statement - ", "")
208216
.replace("Another context - ", "")
@@ -226,8 +234,7 @@ impl App {
226234

227235
let proposals_paragraph = Paragraph::new(proposal_lines)
228236
.style(self.theme.ratatui_style(Element::Text))
229-
.wrap(Wrap { trim: true })
230-
.scroll((self.current_proposal_index as u16 * 3, 0)); // Scroll to selected proposal
237+
.wrap(Wrap { trim: true });
231238

232239
frame.render_widget(proposals_paragraph, chunks[1]);
233240

@@ -241,11 +248,7 @@ impl App {
241248
}
242249

243250
fn render_coaching_tip_modal(&self, frame: &mut ratatui::Frame, area: Rect) {
244-
use ratatui::{
245-
prelude::Alignment,
246-
text::{Line, Span},
247-
widgets::Paragraph,
248-
};
251+
use ratatui::{prelude::Alignment, text::Line, widgets::Paragraph};
249252

250253
let block = Block::default()
251254
.title(" Coaching Tip ")
@@ -267,8 +270,9 @@ impl App {
267270
// Main coaching message with tips
268271
let message_text = vec![
269272
Line::from(""),
270-
Line::from("Ruixen is having a tough time with this abstract query. Smaller"),
271-
Line::from("local models work best with clear and concrete questions."),
273+
Line::from("Ruixen is having a tough time with this abstract query."),
274+
Line::from(""),
275+
Line::from(":: Smaller local models work best with clear and concrete questions."),
272276
Line::from(""),
273277
Line::from(":: Try a more direct question, add specific context, or break"),
274278
Line::from(" the query down into smaller steps."),
@@ -463,10 +467,13 @@ impl App {
463467
frame.render_widget(Clear, modal_area);
464468
self.render_coaching_tip_modal(frame, modal_area);
465469
} else if self.mode == AppMode::Complete {
466-
let block = Block::default().title("Final Prompt").borders(Borders::ALL);
467-
let paragraph = Paragraph::new(self.final_prompt.as_str())
470+
let block = Block::default()
471+
.title("Synthesis Complete")
472+
.borders(Borders::ALL);
473+
let paragraph = Paragraph::new(self.cloud_response.as_str())
468474
.block(block)
469-
.wrap(Wrap { trim: true });
475+
.wrap(Wrap { trim: true })
476+
.scroll((self.synthesis_scroll, 0));
470477
frame.render_widget(paragraph, app_chunks[1]);
471478
} else {
472479
render_chat(
@@ -600,6 +607,16 @@ impl App {
600607
// TODO: Set error state and display to user
601608
self.agent_status = AgentStatus::Ready;
602609
}
610+
AgentMessage::CloudSynthesisComplete(Ok(response)) => {
611+
self.cloud_response = response;
612+
self.mode = AppMode::Complete;
613+
self.agent_status = AgentStatus::Complete;
614+
}
615+
AgentMessage::CloudSynthesisComplete(Err(_e)) => {
616+
// Show coaching tip for cloud API failures
617+
self.mode = AppMode::CoachingTip;
618+
self.agent_status = AgentStatus::Ready;
619+
}
603620
}
604621
}
605622

@@ -841,12 +858,12 @@ impl App {
841858
}
842859
}
843860
KeyCode::Enter => {
844-
// Synthesize - use selected proposal
861+
// Synthesize - send proposal to cloud for synthesis
845862
if let Some(proposal) =
846863
self.proposals.get(self.current_proposal_index)
847864
{
848865
self.final_prompt = proposal.clone();
849-
self.mode = AppMode::Complete;
866+
self.handle_cloud_synthesis();
850867
}
851868
}
852869
KeyCode::Char('e') => {
@@ -879,11 +896,20 @@ impl App {
879896
_ => {}
880897
},
881898
AppMode::Complete => match key.code {
899+
KeyCode::Up => {
900+
self.synthesis_scroll = self.synthesis_scroll.saturating_sub(1);
901+
}
902+
KeyCode::Down => {
903+
self.synthesis_scroll = self.synthesis_scroll.saturating_add(1);
904+
}
882905
KeyCode::Enter | KeyCode::Esc => {
883906
self.mode = AppMode::Normal;
884907
self.final_prompt.clear();
885908
self.proposals.clear();
886909
self.current_proposal_index = 0;
910+
self.cloud_response.clear();
911+
self.synthesis_scroll = 0;
912+
self.agent_status = AgentStatus::Ready;
887913
}
888914
_ => {}
889915
},
@@ -947,6 +973,21 @@ impl App {
947973
self.edit_buffer.clear();
948974
}
949975

976+
fn handle_cloud_synthesis(&mut self) {
977+
// Set status to searching and trigger cloud API call
978+
self.agent_status = AgentStatus::Searching;
979+
980+
let prompt = self.final_prompt.clone();
981+
let api_key = self.settings.api_key.clone();
982+
let model = self.settings.cloud_model.clone();
983+
let tx = self.agent_tx.clone();
984+
985+
tokio::spawn(async move {
986+
let result = cloud::call_cloud_model(&api_key, &model, &prompt).await;
987+
let _ = tx.send(AgentMessage::CloudSynthesisComplete(result));
988+
});
989+
}
990+
950991
fn handle_slash_command(&mut self, command: &str) {
951992
match command {
952993
"/setting" | "/settings" => {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ fn build_smart_status_with_color(status: AgentStatus, settings: &Settings) -> (S
103103
AgentStatus::Orchestrating => {
104104
format!("Ruixen :: [ORCHESTRATING] :: {}", cloud_display)
105105
}
106+
AgentStatus::Searching => {
107+
format!("Ruixen :: ☁️🔍 :: [SEARCHING] :: {}", cloud_display)
108+
}
109+
AgentStatus::Complete => {
110+
format!("Ruixen :: ✨ :: [SYNTHESIS COMPLETE] :: {}", cloud_display)
111+
}
106112
};
107113

108114
let color = match status {
@@ -117,6 +123,8 @@ fn build_smart_status_with_color(status: AgentStatus, settings: &Settings) -> (S
117123
}
118124
AgentStatus::ValidatingLocal | AgentStatus::ValidatingCloud => Color::Yellow, // Testing in progress
119125
AgentStatus::Orchestrating => Color::Cyan,
126+
AgentStatus::Searching => Color::Blue, // Cloud synthesis in progress
127+
AgentStatus::Complete => Color::Green, // Success!
120128
AgentStatus::LocalEndpointError | AgentStatus::CloudEndpointError => Color::Red, // Connection failed
121129
_ => Color::Red, // Other validation failed
122130
};

0 commit comments

Comments
 (0)