diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-13 17:21:21 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-13 17:21:21 +0200 |
| commit | 4961aae1737b77ff77102d37bf5f37f31a43824e (patch) | |
| tree | e344777946a6d4354312ffb638ece6e9cbbf3a0e | |
| parent | make cron follow local time zone (diff) | |
| download | senator-sperma-4961aae1737b77ff77102d37bf5f37f31a43824e.tar.gz senator-sperma-4961aae1737b77ff77102d37bf5f37f31a43824e.zip | |
fix assistant and senator
Signed-off-by: salmonik <salmonik@tuta.io>
| -rw-r--r-- | src/commands/ai.rs | 18 | ||||
| -rw-r--r-- | src/config.rs | 63 | ||||
| -rw-r--r-- | src/events/mod.rs | 18 | ||||
| -rw-r--r-- | src/lang.rs | 4 | ||||
| -rw-r--r-- | src/openrouter/mod.rs | 88 |
5 files changed, 117 insertions, 74 deletions
diff --git a/src/commands/ai.rs b/src/commands/ai.rs index a75f164..5ab077e 100644 --- a/src/commands/ai.rs +++ b/src/commands/ai.rs | |||
| @@ -4,7 +4,7 @@ use crate::lang; | |||
| 4 | 4 | ||
| 5 | type Context<'a> = poise::Context<'a, Data, Error>; | 5 | type Context<'a> = poise::Context<'a, Data, Error>; |
| 6 | 6 | ||
| 7 | /// Query the AI senator via OpenRouter | 7 | /// Query the AI senator via OpenRouter or local LLM |
| 8 | #[poise::command(slash_command)] | 8 | #[poise::command(slash_command)] |
| 9 | pub async fn senator( | 9 | pub async fn senator( |
| 10 | ctx: Context<'_>, | 10 | ctx: Context<'_>, |
| @@ -13,20 +13,8 @@ pub async fn senator( | |||
| 13 | ) -> crate::error::Result<()> { | 13 | ) -> crate::error::Result<()> { |
| 14 | ctx.defer().await?; | 14 | ctx.defer().await?; |
| 15 | 15 | ||
| 16 | let api_key = ctx | 16 | let (api_url, api_key, model) = ctx.data().config.resolve_llm(); |
| 17 | .data() | 17 | let client = crate::openrouter::OpenRouterClient::new(api_url, api_key, model); |
| 18 | .config | ||
| 19 | .openrouter_api_key | ||
| 20 | .as_ref() | ||
| 21 | .ok_or_else(|| Error::Custom(lang::L_OPENROUTER_NO_KEY.into()))?; | ||
| 22 | let model = ctx | ||
| 23 | .data() | ||
| 24 | .config | ||
| 25 | .openrouter_model | ||
| 26 | .as_deref() | ||
| 27 | .unwrap_or("cognitivecomputations/dolphin-mistral-24b-venice-edition:free"); | ||
| 28 | |||
| 29 | let client = crate::openrouter::OpenRouterClient::new(api_key.clone(), model.to_string()); | ||
| 30 | let response = client.chat(lang::SENATOR_SYSTEM_PROMPT, &text).await?; | 18 | let response = client.chat(lang::SENATOR_SYSTEM_PROMPT, &text).await?; |
| 31 | 19 | ||
| 32 | if tts_vc.unwrap_or(false) { | 20 | if tts_vc.unwrap_or(false) { |
diff --git a/src/config.rs b/src/config.rs index 96a6849..989a060 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -10,8 +10,13 @@ pub struct Config { | |||
| 10 | pub copyparty_url: String, | 10 | pub copyparty_url: String, |
| 11 | pub copyparty_url_upload: String, | 11 | pub copyparty_url_upload: String, |
| 12 | pub copyparty_password: Option<String>, | 12 | pub copyparty_password: Option<String>, |
| 13 | /// Deprecated – use llm_api_key instead | ||
| 13 | pub openrouter_api_key: Option<String>, | 14 | pub openrouter_api_key: Option<String>, |
| 15 | /// Deprecated – use llm_model instead | ||
| 14 | pub openrouter_model: Option<String>, | 16 | pub openrouter_model: Option<String>, |
| 17 | pub llm_api_key: Option<String>, | ||
| 18 | pub llm_model: Option<String>, | ||
| 19 | pub llm_api_url: String, | ||
| 15 | pub database_path: String, | 20 | pub database_path: String, |
| 16 | } | 21 | } |
| 17 | 22 | ||
| @@ -72,6 +77,24 @@ const ENV_VARS: &[EnvVar] = &[ | |||
| 72 | description: "OpenRouter model", | 77 | description: "OpenRouter model", |
| 73 | }, | 78 | }, |
| 74 | EnvVar { | 79 | EnvVar { |
| 80 | name: "LLM_API_KEY", | ||
| 81 | required: false, | ||
| 82 | default: None, | ||
| 83 | description: "LLM API key (takes priority over OPENROUTER_API_KEY)", | ||
| 84 | }, | ||
| 85 | EnvVar { | ||
| 86 | name: "LLM_MODEL", | ||
| 87 | required: false, | ||
| 88 | default: None, | ||
| 89 | description: "LLM model name (takes priority over OPENROUTER_MODEL)", | ||
| 90 | }, | ||
| 91 | EnvVar { | ||
| 92 | name: "LLM_API_URL", | ||
| 93 | required: false, | ||
| 94 | default: Some("https://openrouter.ai/api/v1/chat/completions"), | ||
| 95 | description: "LLM API full URL including /chat/completions path", | ||
| 96 | }, | ||
| 97 | EnvVar { | ||
| 75 | name: "SENATOR_DB_PATH", | 98 | name: "SENATOR_DB_PATH", |
| 76 | required: false, | 99 | required: false, |
| 77 | default: Some("data/senator.db"), | 100 | default: Some("data/senator.db"), |
| @@ -126,6 +149,19 @@ pub fn check_env_vars() { | |||
| 126 | 149 | ||
| 127 | impl Config { | 150 | impl Config { |
| 128 | pub fn from_env() -> Self { | 151 | pub fn from_env() -> Self { |
| 152 | let openrouter_api_key = env::var("OPENROUTER_API_KEY").ok(); | ||
| 153 | let openrouter_model = env::var("OPENROUTER_MODEL").ok(); | ||
| 154 | let llm_api_key = env::var("LLM_API_KEY").ok(); | ||
| 155 | let llm_model = env::var("LLM_MODEL").ok(); | ||
| 156 | |||
| 157 | // Warn about deprecated env vars (mirrors Python's BACKWARD_COMPAT_MSG) | ||
| 158 | if openrouter_api_key.is_some() && llm_api_key.is_none() { | ||
| 159 | tracing::warn!("OPENROUTER_API_KEY is deprecated. Use LLM_API_KEY instead."); | ||
| 160 | } | ||
| 161 | if openrouter_model.is_some() && llm_model.is_none() { | ||
| 162 | tracing::warn!("OPENROUTER_MODEL is deprecated. Use LLM_MODEL instead."); | ||
| 163 | } | ||
| 164 | |||
| 129 | Self { | 165 | Self { |
| 130 | token: env::var("TOKEN").expect("Missing TOKEN"), | 166 | token: env::var("TOKEN").expect("Missing TOKEN"), |
| 131 | status_channel_id: env::var("STATUS_CHANNEL_ID") | 167 | status_channel_id: env::var("STATUS_CHANNEL_ID") |
| @@ -137,9 +173,32 @@ impl Config { | |||
| 137 | copyparty_url_upload: env::var("COPYPARTY_URL_UPLOAD") | 173 | copyparty_url_upload: env::var("COPYPARTY_URL_UPLOAD") |
| 138 | .unwrap_or_else(|_| "http://localhost:3939".into()), | 174 | .unwrap_or_else(|_| "http://localhost:3939".into()), |
| 139 | copyparty_password: env::var("COPYPARTY_PASSWORD").ok(), | 175 | copyparty_password: env::var("COPYPARTY_PASSWORD").ok(), |
| 140 | openrouter_api_key: env::var("OPENROUTER_API_KEY").ok(), | 176 | openrouter_api_key, |
| 141 | openrouter_model: env::var("OPENROUTER_MODEL").ok(), | 177 | openrouter_model, |
| 178 | llm_api_key, | ||
| 179 | llm_model, | ||
| 180 | llm_api_url: env::var("LLM_API_URL") | ||
| 181 | .unwrap_or_else(|_| "https://openrouter.ai/api/v1/chat/completions".into()), | ||
| 142 | database_path: env::var("SENATOR_DB_PATH").unwrap_or_else(|_| "data/senator.db".into()), | 182 | database_path: env::var("SENATOR_DB_PATH").unwrap_or_else(|_| "data/senator.db".into()), |
| 143 | } | 183 | } |
| 144 | } | 184 | } |
| 185 | |||
| 186 | /// Resolve the effective LLM config: (api_url, api_key, model). | ||
| 187 | /// LLM_* vars take priority; falls back to OPENROUTER_* for backward compat. | ||
| 188 | pub fn resolve_llm(&self) -> (String, String, String) { | ||
| 189 | let api_url = self.llm_api_url.clone(); | ||
| 190 | let api_key = self | ||
| 191 | .llm_api_key | ||
| 192 | .clone() | ||
| 193 | .or_else(|| self.openrouter_api_key.clone()) | ||
| 194 | .unwrap_or_default(); | ||
| 195 | let model = self | ||
| 196 | .llm_model | ||
| 197 | .clone() | ||
| 198 | .or_else(|| self.openrouter_model.clone()) | ||
| 199 | .unwrap_or_else(|| { | ||
| 200 | "cognitivecomputations/dolphin-mistral-24b-venice-edition:free".into() | ||
| 201 | }); | ||
| 202 | (api_url, api_key, model) | ||
| 203 | } | ||
| 145 | } | 204 | } |
diff --git a/src/events/mod.rs b/src/events/mod.rs index 457ebcd..9a193d0 100644 --- a/src/events/mod.rs +++ b/src/events/mod.rs | |||
| @@ -335,20 +335,22 @@ pub async fn handle_message( | |||
| 335 | let is_mention = new_message.mentions_user_id(ctx.cache.current_user().id); | 335 | let is_mention = new_message.mentions_user_id(ctx.cache.current_user().id); |
| 336 | let has_keyword = content_lower.contains(&data.config.assistant_keyword); | 336 | let has_keyword = content_lower.contains(&data.config.assistant_keyword); |
| 337 | 337 | ||
| 338 | if (is_mention || has_keyword) && data.config.openrouter_api_key.is_some() { | 338 | if is_mention || has_keyword { |
| 339 | let api_key = data.config.openrouter_api_key.clone().unwrap(); | 339 | let (api_url, api_key, model) = data.config.resolve_llm(); |
| 340 | let model = data.config.openrouter_model.clone().unwrap_or_else(|| { | 340 | let client = crate::openrouter::OpenRouterClient::new(api_url, api_key, model); |
| 341 | "cognitivecomputations/dolphin-mistral-24b-venice-edition:free".into() | ||
| 342 | }); | ||
| 343 | |||
| 344 | let client = crate::openrouter::OpenRouterClient::new(api_key, model); | ||
| 345 | let assistant_prompt = lang::ASSISTANT_SYSTEM_PROMPT; | 341 | let assistant_prompt = lang::ASSISTANT_SYSTEM_PROMPT; |
| 346 | 342 | ||
| 343 | let bot_id = ctx.cache.current_user().id; | ||
| 347 | let mut messages = Vec::new(); | 344 | let mut messages = Vec::new(); |
| 348 | let mut current_msg = Some(new_message.clone()); | 345 | let mut current_msg = Some(new_message.clone()); |
| 349 | for _ in 0..5 { | 346 | for _ in 0..5 { |
| 350 | if let Some(msg) = current_msg { | 347 | if let Some(msg) = current_msg { |
| 351 | messages.push(("user".into(), msg.content.clone())); | 348 | let role = if msg.author.id == bot_id { |
| 349 | "assistant" | ||
| 350 | } else { | ||
| 351 | "user" | ||
| 352 | }; | ||
| 353 | messages.push((role.into(), msg.content.clone())); | ||
| 352 | current_msg = match msg.message_reference.and_then(|r| r.message_id) { | 354 | current_msg = match msg.message_reference.and_then(|r| r.message_id) { |
| 353 | Some(ref_id) => msg.channel_id.message(ctx, ref_id).await.ok(), | 355 | Some(ref_id) => msg.channel_id.message(ctx, ref_id).await.ok(), |
| 354 | None => None, | 356 | None => None, |
diff --git a/src/lang.rs b/src/lang.rs index 626f387..b1ed088 100644 --- a/src/lang.rs +++ b/src/lang.rs | |||
| @@ -312,8 +312,8 @@ pub fn fmt_cp_invalid_json(e: &dyn std::fmt::Display, snippet: &str) -> String { | |||
| 312 | 312 | ||
| 313 | // ── OpenRouter ──────────────────────────────────────────────────────── | 313 | // ── OpenRouter ──────────────────────────────────────────────────────── |
| 314 | 314 | ||
| 315 | pub fn fmt_openrouter_err(status: &dyn std::fmt::Display, text: &str) -> String { | 315 | pub fn fmt_llm_err(endpoint: &str, status: &dyn std::fmt::Display, text: &str) -> String { |
| 316 | format!("Błąd API OpenRouter. {status}: {text}") | 316 | format!("Błąd API LLM ({endpoint}). {status}: {text}") |
| 317 | } | 317 | } |
| 318 | 318 | ||
| 319 | // ── Voice / audio decoding ──────────────────────────────────────────── | 319 | // ── Voice / audio decoding ──────────────────────────────────────────── |
diff --git a/src/openrouter/mod.rs b/src/openrouter/mod.rs index 31cee85..1d3b443 100644 --- a/src/openrouter/mod.rs +++ b/src/openrouter/mod.rs | |||
| @@ -1,3 +1,5 @@ | |||
| 1 | use tracing::info; | ||
| 2 | |||
| 1 | use reqwest::Client; | 3 | use reqwest::Client; |
| 2 | use serde::{Deserialize, Serialize}; | 4 | use serde::{Deserialize, Serialize}; |
| 3 | 5 | ||
| @@ -7,6 +9,7 @@ use crate::lang; | |||
| 7 | pub struct OpenRouterClient { | 9 | pub struct OpenRouterClient { |
| 8 | api_key: String, | 10 | api_key: String, |
| 9 | model: String, | 11 | model: String, |
| 12 | api_url: String, | ||
| 10 | client: Client, | 13 | client: Client, |
| 11 | } | 14 | } |
| 12 | 15 | ||
| @@ -39,14 +42,49 @@ struct MessageContent { | |||
| 39 | } | 42 | } |
| 40 | 43 | ||
| 41 | impl OpenRouterClient { | 44 | impl OpenRouterClient { |
| 42 | pub fn new(api_key: String, model: String) -> Self { | 45 | /// `api_url` is the full URL including `/chat/completions` path. |
| 46 | pub fn new(api_url: String, api_key: String, model: String) -> Self { | ||
| 47 | info!("LLM client: url={api_url}, model={model}"); | ||
| 43 | Self { | 48 | Self { |
| 44 | api_key, | 49 | api_key, |
| 45 | model, | 50 | model, |
| 51 | api_url, | ||
| 46 | client: Client::new(), | 52 | client: Client::new(), |
| 47 | } | 53 | } |
| 48 | } | 54 | } |
| 49 | 55 | ||
| 56 | async fn do_request(&self, body: &ChatRequest) -> Result<String> { | ||
| 57 | let mut req = self | ||
| 58 | .client | ||
| 59 | .post(&self.api_url) | ||
| 60 | .header("Content-Type", "application/json") | ||
| 61 | .json(body); | ||
| 62 | |||
| 63 | if !self.api_key.is_empty() { | ||
| 64 | req = req.header("Authorization", format!("Bearer {}", self.api_key)); | ||
| 65 | } | ||
| 66 | |||
| 67 | let resp = req.send().await?; | ||
| 68 | |||
| 69 | if !resp.status().is_success() { | ||
| 70 | let status = resp.status(); | ||
| 71 | let text = resp.text().await.unwrap_or_default(); | ||
| 72 | return Err(crate::error::Error::Custom(lang::fmt_llm_err( | ||
| 73 | &self.api_url, | ||
| 74 | &status, | ||
| 75 | &text, | ||
| 76 | ))); | ||
| 77 | } | ||
| 78 | |||
| 79 | let chat_resp: ChatResponse = resp.json().await?; | ||
| 80 | Ok(chat_resp | ||
| 81 | .choices | ||
| 82 | .into_iter() | ||
| 83 | .next() | ||
| 84 | .map(|c| c.message.content) | ||
| 85 | .unwrap_or_default()) | ||
| 86 | } | ||
| 87 | |||
| 50 | pub async fn chat(&self, system_prompt: &str, user_message: &str) -> Result<String> { | 88 | pub async fn chat(&self, system_prompt: &str, user_message: &str) -> Result<String> { |
| 51 | let body = ChatRequest { | 89 | let body = ChatRequest { |
| 52 | model: self.model.clone(), | 90 | model: self.model.clone(), |
| @@ -63,29 +101,7 @@ impl OpenRouterClient { | |||
| 63 | max_tokens: 500, | 101 | max_tokens: 500, |
| 64 | }; | 102 | }; |
| 65 | 103 | ||
| 66 | let resp = self | 104 | self.do_request(&body).await |
| 67 | .client | ||
| 68 | .post("https://openrouter.ai/api/v1/chat/completions") | ||
| 69 | .header("Authorization", format!("Bearer {}", self.api_key)) | ||
| 70 | .json(&body) | ||
| 71 | .send() | ||
| 72 | .await?; | ||
| 73 | |||
| 74 | if !resp.status().is_success() { | ||
| 75 | let status = resp.status(); | ||
| 76 | let text = resp.text().await.unwrap_or_default(); | ||
| 77 | return Err(crate::error::Error::Custom(lang::fmt_openrouter_err( | ||
| 78 | &status, &text, | ||
| 79 | ))); | ||
| 80 | } | ||
| 81 | |||
| 82 | let chat_resp: ChatResponse = resp.json().await?; | ||
| 83 | Ok(chat_resp | ||
| 84 | .choices | ||
| 85 | .into_iter() | ||
| 86 | .next() | ||
| 87 | .map(|c| c.message.content) | ||
| 88 | .unwrap_or_default()) | ||
| 89 | } | 105 | } |
| 90 | 106 | ||
| 91 | pub async fn chat_with_history( | 107 | pub async fn chat_with_history( |
| @@ -107,28 +123,6 @@ impl OpenRouterClient { | |||
| 107 | max_tokens: 500, | 123 | max_tokens: 500, |
| 108 | }; | 124 | }; |
| 109 | 125 | ||
| 110 | let resp = self | 126 | self.do_request(&body).await |
| 111 | .client | ||
| 112 | .post("https://openrouter.ai/api/v1/chat/completions") | ||
| 113 | .header("Authorization", format!("Bearer {}", self.api_key)) | ||
| 114 | .json(&body) | ||
| 115 | .send() | ||
| 116 | .await?; | ||
| 117 | |||
| 118 | if !resp.status().is_success() { | ||
| 119 | let status = resp.status(); | ||
| 120 | let text = resp.text().await.unwrap_or_default(); | ||
| 121 | return Err(crate::error::Error::Custom(lang::fmt_openrouter_err( | ||
| 122 | &status, &text, | ||
| 123 | ))); | ||
| 124 | } | ||
| 125 | |||
| 126 | let chat_resp: ChatResponse = resp.json().await?; | ||
| 127 | Ok(chat_resp | ||
| 128 | .choices | ||
| 129 | .into_iter() | ||
| 130 | .next() | ||
| 131 | .map(|c| c.message.content) | ||
| 132 | .unwrap_or_default()) | ||
| 133 | } | 127 | } |
| 134 | } | 128 | } |
