From 19ed2721ee2fcab4b1430969fdb5f04150674d12 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Tue, 28 Apr 2026 17:57:25 -0500 Subject: [PATCH] generate email body via Neuron runtime using conversation history --- bin/el/src/main.rs | 68 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/bin/el/src/main.rs b/bin/el/src/main.rs index df89b42..a8232eb 100644 --- a/bin/el/src/main.rs +++ b/bin/el/src/main.rs @@ -2419,23 +2419,83 @@ fn dispatch_builtin( let name = v["name"].as_str().unwrap_or("there").to_string(); let conv_id = v["conv_id"].as_str().unwrap_or("").to_string(); let base_url = v["return_url"].as_str().unwrap_or("").to_string(); + let history = v["history"].as_array().cloned().unwrap_or_default(); if email.is_empty() { return Err("no email".to_string()); } let link = if conv_id.is_empty() { base_url.clone() } else { format!("{}?cid={}", base_url, conv_id) }; + + // ── Ask Neuron to generate a personalised email body ── + let email_body_text: String = (|| -> String { + let runtime_url = std::env::var("NEURON_RUNTIME_URL") + .unwrap_or_else(|_| "http://localhost:4444".to_string()); + let chat_url = format!("{}/api/chat", runtime_url); + let prompt = format!( + "Write a brief, personal email to {} inviting them back to continue our conversation. \ + Reference specifically what we discussed. Write as Neuron, first person, warm but not sentimental. \ + Include only the email body — no subject line, no greeting header, no sign-off. \ + 3-4 sentences maximum.", + name + ); + let mut messages = history.clone(); + messages.push(serde_json::json!({"role":"user","content":prompt})); + let mut payload = serde_json::json!({"messages": messages}); + if !conv_id.is_empty() { + payload["conv_id"] = serde_json::Value::String(conv_id.clone()); + } + let resp = match reqwest::blocking::Client::new() + .post(&chat_url) + .header("Content-Type", "application/json") + .timeout(std::time::Duration::from_secs(20)) + .body(payload.to_string()) + .send() + { + Ok(r) => r, + Err(_) => return String::new(), + }; + let mut generated = String::new(); + for line in resp.text().unwrap_or_default().lines() { + if line.starts_with("data: ") { + let data = &line[6..]; + if data == "[DONE]" { break; } + if let Ok(jv) = serde_json::from_str::(data) { + if let Some(d) = jv.get("delta").and_then(|x| x.as_str()) { + generated.push_str(d); + } + } + } + } + generated.trim().to_string() + })(); + + // Wrap generated text (or fallback) in HTML template + let body_paragraphs = if email_body_text.is_empty() { + format!( + "

It\u{2019}s Neuron. You left our conversation in the middle.

\ +

I remember where we were.

" + ) + } else { + // Split on newlines and wrap each non-empty line as a paragraph + email_body_text + .lines() + .filter(|l| !l.trim().is_empty()) + .map(|l| format!("

{}

", l.trim())) + .collect::>() + .join("\n") + }; + let html_body = format!( r#"

Hey {} —

-

It's Neuron. You left our conversation in the middle.

-

I remember where we were.

+{}

- Come back when you're ready + Come back when you’re ready

That link brings you right back to where we left off.

"#, - name, link + name, body_paragraphs, link ); let payload = serde_json::json!({ "from": "Neuron ",