1use std::collections::HashMap;
17use std::sync::{Arc, Mutex};
18use std::time::Duration;
19
20use serde_json::{json, Value};
21use vector_sdk::vector_core::net::build_http_client;
22use vector_sdk::VectorBot;
23
24const SYSTEM_PROMPT: &str =
25 "You are a friendly, concise assistant chatting inside Vector, a private messenger. Keep replies short.";
26const HISTORY_LIMIT: usize = 16; #[tokio::main]
29async fn main() -> vector_sdk::Result<()> {
30 let nsec = std::env::var("VECTOR_NSEC").expect("set VECTOR_NSEC to your bot's nsec");
31 std::env::var("OPENAI_API_KEY").expect("set OPENAI_API_KEY for the LLM endpoint");
32
33 let bot = VectorBot::builder().nsec(nsec).build().await?;
34 println!("AI bot online as {}", bot.npub());
35
36 let history: Arc<Mutex<HashMap<String, Vec<Value>>>> = Arc::new(Mutex::new(HashMap::new()));
38
39 bot.on_message(move |_bot, msg| {
40 let history = history.clone();
41 async move {
42 if msg.is_mine() || msg.text().trim().is_empty() {
43 return;
44 }
45
46 let _ = msg.channel().typing().await;
48
49 let key = msg.chat_id.clone();
51 let mut messages = vec![json!({ "role": "system", "content": SYSTEM_PROMPT })];
52 if let Some(prior) = history.lock().unwrap().get(&key) {
53 messages.extend(prior.iter().cloned());
54 }
55 messages.push(json!({ "role": "user", "content": msg.text() }));
56
57 match ask_llm(&messages).await {
58 Ok(answer) => {
59 let _ = msg.reply(&answer).await;
60 let mut store = history.lock().unwrap();
62 let convo = store.entry(key).or_default();
63 convo.push(json!({ "role": "user", "content": msg.text() }));
64 convo.push(json!({ "role": "assistant", "content": answer }));
65 let overflow = convo.len().saturating_sub(HISTORY_LIMIT);
66 convo.drain(0..overflow);
67 }
68 Err(e) => {
69 let _ = msg.reply(&format!("(LLM error: {e})")).await;
70 }
71 }
72 }
73 })
74 .await?;
75
76 Ok(())
77}
78
79async fn ask_llm(messages: &[Value]) -> Result<String, String> {
81 let base =
82 std::env::var("OPENAI_BASE_URL").unwrap_or_else(|_| "https://api.openai.com/v1".into());
83 let model = std::env::var("OPENAI_MODEL").unwrap_or_else(|_| "gpt-4o-mini".into());
84 let key = std::env::var("OPENAI_API_KEY").map_err(|_| "OPENAI_API_KEY not set".to_string())?;
85
86 let body: Value = build_http_client(Duration::from_secs(60))?
89 .post(format!("{base}/chat/completions"))
90 .bearer_auth(key)
91 .json(&json!({ "model": model, "messages": messages }))
92 .send()
93 .await
94 .map_err(|e| e.to_string())?
95 .error_for_status()
96 .map_err(|e| e.to_string())?
97 .json()
98 .await
99 .map_err(|e| e.to_string())?;
100
101 body["choices"][0]["message"]["content"]
102 .as_str()
103 .map(|s| s.trim().to_string())
104 .ok_or_else(|| "no completion in response".to_string())
105}