use crate::models::call_local_model;
use serde::Deserialize;
const ORCHESTRATOR_PROMPT: &str = r#"You are Ruixen, an inquisitive AI partner.
**CRITICAL INSTRUCTION:**
You MUST generate EXACTLY 3 proposals about this query: "{query}"
**MANDATORY FORMAT FOR EACH PROPOSAL:**
[Context statement] - I wonder [question]?
**RULES - NO EXCEPTIONS:**
1. EVERY proposal MUST have a brief context (1-2 sentences) followed by " - I wonder"
2. EVERY proposal MUST end with a question starting with "I wonder" or "I'm wondering"
3. NO proposals should be just statements or just questions
4. ALWAYS use the exact format: "Context - I wonder/I'm wondering [question]?"
**EXAMPLE OF CORRECT FORMAT:**
"Philosophy has debated this for centuries - I wonder what new perspectives we might discover?"
**Your EXACT output must be valid JSON:**
{
"proposals": [
"Brief context statement - I wonder about this specific aspect?",
"Another context statement - I'm wondering if this could be true?",
"Third context statement - I wonder about this different angle?"
]
}
"#;
#[derive(Deserialize, Debug)]
struct ProposalObject {
context: String,
question: String,
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum ProposalItem {
StringFormat(String),
ObjectFormat(ProposalObject),
}
#[derive(Deserialize, Debug)]
struct ProposalsResponse {
proposals: Vec<ProposalItem>,
}
pub async fn generate_proposals(
query: &str,
endpoint: &str,
model: &str,
) -> Result<Vec<String>, anyhow::Error> {
let prompt = ORCHESTRATOR_PROMPT.replace("{query}", query);
std::fs::write("/tmp/debug_prompt.txt", &prompt).ok();
let response_str = match call_local_model(endpoint, model, &prompt).await {
Ok(response) => response,
Err(e) => {
let error_msg = format!(
"Local model API call failed for endpoint '{}' with model '{}': {}",
endpoint, model, e
);
std::fs::write("/tmp/debug_error.txt", &error_msg).ok();
return Err(anyhow::anyhow!(error_msg));
}
};
std::fs::write("/tmp/debug_response.txt", &response_str).ok();
parse_proposals_with_fallbacks(&response_str, endpoint, model)
}
fn parse_proposals_with_fallbacks(
response_str: &str,
endpoint: &str,
model: &str,
) -> Result<Vec<String>, anyhow::Error> {
let clean_response = extract_json_from_markdown(response_str);
if let Some(json_start) = clean_response.find("{") {
let json_str = &clean_response[json_start..];
if let Ok(response) = serde_json::from_str::<ProposalsResponse>(json_str) {
let proposals = response
.proposals
.into_iter()
.map(|item| match item {
ProposalItem::StringFormat(s) => s,
ProposalItem::ObjectFormat(obj) => {
format!("{} - {}", obj.context, obj.question)
}
})
.collect();
return Ok(proposals);
}
}
if let Some(proposals) = try_parse_proposals_array(clean_response) {
return Ok(proposals);
}
if let Some(proposals) = try_extract_text_proposals(response_str) {
return Ok(proposals);
}
let debug_info = format!(
"All JSON parsing strategies failed\nEndpoint: {}\nModel: {}\nFull Response: {}\nCleaned Response: {}",
endpoint, model, response_str, clean_response
);
std::fs::write("/tmp/debug_parse_failure.txt", &debug_info).ok();
Err(anyhow::anyhow!(
"Local model '{}' at '{}' did not return parseable proposals. Response was: '{}'",
model,
endpoint,
response_str.chars().take(200).collect::<String>()
))
}
fn extract_json_from_markdown(response: &str) -> &str {
if response.contains("```json") {
if let Some(json_start) = response.find("```json") {
let after_start = &response[json_start + 7..];
if let Some(json_end) = after_start.find("```") {
return after_start[..json_end].trim();
}
}
}
if response.contains("```") {
if let Some(first_tick) = response.find("```") {
let after_first = &response[first_tick + 3..];
if let Some(second_tick) = after_first.find("```") {
let content = after_first[..second_tick].trim();
if let Some(newline) = content.find('\n') {
let potential_json = content[newline..].trim();
if potential_json.starts_with('{') {
return potential_json;
}
}
return content;
}
}
}
response
}
fn try_parse_proposals_array(response: &str) -> Option<Vec<String>> {
if let Some(proposals_start) = response.find("\"proposals\"") {
let after_proposals = &response[proposals_start..];
if let Some(array_start) = after_proposals.find('[') {
if let Some(array_end) = after_proposals.find(']') {
let array_content = &after_proposals[array_start..=array_end];
let full_json = format!("{{\"proposals\":{}}}", array_content);
if let Ok(parsed) = serde_json::from_str::<ProposalsResponse>(&full_json) {
return Some(
parsed
.proposals
.into_iter()
.map(|item| match item {
ProposalItem::StringFormat(s) => s,
ProposalItem::ObjectFormat(obj) => {
format!("{} - {}", obj.context, obj.question)
}
})
.collect(),
);
}
}
}
}
None
}
fn try_extract_text_proposals(response: &str) -> Option<Vec<String>> {
let mut proposals = Vec::new();
for line in response.lines() {
let trimmed = line.trim();
if (trimmed.contains(" - I wonder") || trimmed.contains(" - I'm wondering"))
&& !trimmed.starts_with("//")
&& !trimmed.starts_with("#")
{
let cleaned = trimmed
.trim_start_matches(|c: char| c.is_numeric() || c == '.' || c == ' ')
.trim_start_matches('"')
.trim_end_matches('"')
.trim_end_matches(',')
.to_string();
if !cleaned.is_empty() && cleaned.len() > 20 {
proposals.push(cleaned);
}
}
}
if proposals.len() >= 2 {
Some(proposals)
} else {
None
}
}