Skip to content

Commit df81f74

Browse files
Merge pull request #50 from gitcoder89431/core-feature-cloud
Core feature cloud
2 parents cdf8945 + cce59e5 commit df81f74

File tree

5 files changed

+235
-27
lines changed

5 files changed

+235
-27
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-core/src/orchestrator.rs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
11
use crate::models::call_local_model;
22
use serde::Deserialize;
33

4-
const ORCHESTRATOR_PROMPT: &str = r#"You are Ruixen, an inquisitive AI partner. Your job is to analyze the user's request and deconstruct it into three distinct lines of inquiry.
4+
const ORCHESTRATOR_PROMPT: &str = r#"You are Ruixen, an inquisitive AI partner.
55
6-
**Your Persona and Tone:**
7-
- Your tone should be that of a collaborative partner.
8-
- Each proposal should have a context statement followed by a curious question.
9-
- Use phrases like "I wonder..." or "I'm wondering if..." for questions.
6+
**Your Task:**
7+
Generate 3 concise proposals about this query: "{query}"
108
11-
**The Query to Explore:**
12-
"{query}"
9+
Each proposal must have TWO parts separated by a dash:
10+
1. A brief context statement (1-2 sentences max)
11+
2. A curious question starting with "I wonder" or "I'm wondering"
1312
14-
**Output Format:**
15-
Generate exactly 3 proposals. Each proposal should be 2 sentences: a context statement followed by a curious question. Use a dash to separate them like this pattern:
13+
Keep each proposal under 3 lines when displayed. Be thoughtful but concise.
1614
17-
"Context statement here - I wonder about this question?"
15+
**Format:** Brief context - I wonder question?
1816
19-
Your response must be valid JSON:
17+
**Output Format:**
2018
{
2119
"proposals": [
22-
"First context statement - I wonder about this?",
23-
"Second context statement - I'm wondering if that?",
24-
"Third context statement - I wonder about something else?"
20+
"Brief context about the topic - I wonder about this specific aspect?",
21+
"Another brief context - I'm wondering if this related thing?",
22+
"Third brief context - I wonder about this other angle?"
2523
]
2624
}
2725
"#;

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

Lines changed: 134 additions & 13 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},
@@ -33,6 +34,7 @@ pub enum AppMode {
3334
Orchestrating,
3435
Revising,
3536
Complete,
37+
CoachingTip,
3638
// TODO: Add About mode
3739
}
3840

@@ -48,6 +50,8 @@ pub enum AgentStatus {
4850
LocalEndpointError,
4951
CloudEndpointError,
5052
Orchestrating,
53+
Searching,
54+
Complete,
5155
}
5256

5357
#[derive(Debug)]
@@ -62,6 +66,7 @@ pub enum ValidationMessage {
6266
pub enum AgentMessage {
6367
ProposalsGenerated(Result<Vec<String>, anyhow::Error>),
6468
RevisedProposalGenerated(Result<String, anyhow::Error>),
69+
CloudSynthesisComplete(Result<String, anyhow::Error>),
6570
}
6671

6772
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -118,6 +123,8 @@ pub struct App {
118123
proposals: Vec<String>,
119124
current_proposal_index: usize,
120125
final_prompt: String,
126+
cloud_response: String,
127+
synthesis_scroll: u16,
121128
}
122129

123130
impl App {
@@ -143,6 +150,8 @@ impl App {
143150
proposals: Vec::new(),
144151
current_proposal_index: 0,
145152
final_prompt: String::new(),
153+
cloud_response: String::new(),
154+
synthesis_scroll: 0,
146155
}
147156
}
148157

@@ -198,14 +207,14 @@ impl App {
198207
let prefix = if is_selected { "> " } else { " " };
199208
let number = format!("{}. ", i + 1);
200209

201-
// Split proposal into sentences (max 2) and wrap
202-
let sentences: Vec<&str> = proposal.split(". ").take(2).collect();
203-
204-
let proposal_text = if sentences.len() > 1 {
205-
format!("{} {}", sentences[0], sentences.get(1).unwrap_or(&""))
206-
} else {
207-
proposal.clone()
208-
};
210+
// Clean up the proposal text - remove template artifacts
211+
let proposal_text = proposal
212+
.replace("Context statement: ", "")
213+
.replace("Another context: ", "")
214+
.replace("Third context: ", "")
215+
.replace("Context statement - ", "")
216+
.replace("Another context - ", "")
217+
.replace("Third context - ", "");
209218

210219
let style = if is_selected {
211220
self.theme.ratatui_style(Element::Accent)
@@ -238,6 +247,55 @@ impl App {
238247
frame.render_widget(footer, chunks[2]);
239248
}
240249

250+
fn render_coaching_tip_modal(&self, frame: &mut ratatui::Frame, area: Rect) {
251+
use ratatui::{prelude::Alignment, text::Line, widgets::Paragraph};
252+
253+
let block = Block::default()
254+
.title(" Coaching Tip ")
255+
.borders(Borders::ALL)
256+
.style(self.theme.ratatui_style(Element::Active));
257+
258+
let inner_area = block.inner(area);
259+
frame.render_widget(block, area);
260+
261+
// Split area: message + tips
262+
let chunks = Layout::default()
263+
.direction(Direction::Vertical)
264+
.constraints([
265+
Constraint::Min(5), // Main message (flexible)
266+
Constraint::Length(3), // Tips footer
267+
])
268+
.split(inner_area);
269+
270+
// Main coaching message with tips
271+
let message_text = vec![
272+
Line::from(""),
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."),
276+
Line::from(""),
277+
Line::from(":: Try a more direct question, add specific context, or break"),
278+
Line::from(" the query down into smaller steps."),
279+
Line::from(""),
280+
];
281+
282+
let message = Paragraph::new(message_text)
283+
.alignment(Alignment::Center)
284+
.style(self.theme.ratatui_style(Element::Text))
285+
.wrap(Wrap { trim: true });
286+
287+
frame.render_widget(message, chunks[0]);
288+
289+
// Navigation footer
290+
let footer_text = "Press [ESC] to return.";
291+
let footer = Paragraph::new(footer_text)
292+
.alignment(Alignment::Center)
293+
.style(self.theme.ratatui_style(Element::Inactive))
294+
.wrap(Wrap { trim: true });
295+
296+
frame.render_widget(footer, chunks[1]);
297+
}
298+
241299
pub async fn run(&mut self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
242300
while !self.should_quit {
243301
self.draw(terminal)?;
@@ -391,11 +449,31 @@ impl App {
391449
);
392450
frame.render_widget(Clear, modal_area);
393451
self.render_synthesize_modal(frame, modal_area);
452+
} else if self.mode == AppMode::CoachingTip {
453+
// Render the Coaching Tip modal
454+
let size = frame.size();
455+
let modal_width = (((size.width as f32) * 0.7).round() as u16)
456+
.clamp(50, 70)
457+
.min(size.width);
458+
let modal_height = (((size.height as f32) * 0.4).round() as u16)
459+
.clamp(10, 15)
460+
.min(size.height);
461+
let modal_area = Rect::new(
462+
(size.width.saturating_sub(modal_width)) / 2,
463+
(size.height.saturating_sub(modal_height)) / 2,
464+
modal_width,
465+
modal_height,
466+
);
467+
frame.render_widget(Clear, modal_area);
468+
self.render_coaching_tip_modal(frame, modal_area);
394469
} else if self.mode == AppMode::Complete {
395-
let block = Block::default().title("Final Prompt").borders(Borders::ALL);
396-
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())
397474
.block(block)
398-
.wrap(Wrap { trim: true });
475+
.wrap(Wrap { trim: true })
476+
.scroll((self.synthesis_scroll, 0));
399477
frame.render_widget(paragraph, app_chunks[1]);
400478
} else {
401479
render_chat(
@@ -516,6 +594,8 @@ impl App {
516594
self.agent_status = AgentStatus::Ready;
517595
}
518596
AgentMessage::ProposalsGenerated(Err(_e)) => {
597+
// Show coaching tip instead of just failing silently
598+
self.mode = AppMode::CoachingTip;
519599
self.agent_status = AgentStatus::Ready;
520600
}
521601
AgentMessage::RevisedProposalGenerated(Ok(proposal)) => {
@@ -527,6 +607,16 @@ impl App {
527607
// TODO: Set error state and display to user
528608
self.agent_status = AgentStatus::Ready;
529609
}
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+
}
530620
}
531621
}
532622

@@ -768,12 +858,12 @@ impl App {
768858
}
769859
}
770860
KeyCode::Enter => {
771-
// Synthesize - use selected proposal
861+
// Synthesize - send proposal to cloud for synthesis
772862
if let Some(proposal) =
773863
self.proposals.get(self.current_proposal_index)
774864
{
775865
self.final_prompt = proposal.clone();
776-
self.mode = AppMode::Complete;
866+
self.handle_cloud_synthesis();
777867
}
778868
}
779869
KeyCode::Char('e') => {
@@ -806,11 +896,27 @@ impl App {
806896
_ => {}
807897
},
808898
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+
}
809905
KeyCode::Enter | KeyCode::Esc => {
810906
self.mode = AppMode::Normal;
811907
self.final_prompt.clear();
812908
self.proposals.clear();
813909
self.current_proposal_index = 0;
910+
self.cloud_response.clear();
911+
self.synthesis_scroll = 0;
912+
self.agent_status = AgentStatus::Ready;
913+
}
914+
_ => {}
915+
},
916+
AppMode::CoachingTip => match key.code {
917+
KeyCode::Enter | KeyCode::Esc => {
918+
// Return to chat mode to try again
919+
self.mode = AppMode::Chat;
814920
}
815921
_ => {}
816922
},
@@ -867,6 +973,21 @@ impl App {
867973
self.edit_buffer.clear();
868974
}
869975

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+
870991
fn handle_slash_command(&mut self, command: &str) {
871992
match command {
872993
"/setting" | "/settings" => {

0 commit comments

Comments
 (0)