use serde::{Deserialize, Serialize};
use crate::error::Result;
#[derive(Debug, Clone, Serialize)]
pub struct DebugContext {
pub command: String,
pub query: String,
pub url: String,
pub status: u16,
pub body_snippet: String,
pub error: String,
}
#[derive(Debug, Deserialize)]
struct OllamaGenerateResp {
response: String,
}
pub async fn diagnose_with_llm(ctx: &DebugContext) -> Result<Option<String>> {
if std::env::var("FANFIC_ARCHIVIST_LLM_DEBUG")
.map(|v| v == "0" || v.eq_ignore_ascii_case("false"))
.unwrap_or(false)
{
return Ok(None);
}
let base = std::env::var("FANFIC_ARCHIVIST_OLLAMA_URL")
.unwrap_or_else(|_| "http://localhost:11434".to_string());
let model = std::env::var("FANFIC_ARCHIVIST_LLM_MODEL")
.unwrap_or_else(|_| "ornith:9b".to_string());
let prompt = format!(
"You are diagnosing a failed Discord bot request to a fanfiction \
archive API. The bot called the API and it failed.\n\n\
Command: {cmd}\nQuery: {q}\nURL requested: {url}\nHTTP status: {status}\n\
Raw body (first 800 chars): {body}\nBot error: {err}\n\n\
Diagnose the most likely root cause in 2-3 sentences. Be concrete: \
name the likely bug (wrong URL path, HTML returned instead of JSON, \
rate limit, missing endpoint, etc). Suggest the fix. No markdown.",
cmd = ctx.command,
q = ctx.query,
url = ctx.url,
status = ctx.status,
body = ctx.body_snippet,
err = ctx.error,
);
let client = reqwest::Client::new();
let resp: OllamaGenerateResp = client
.post(format!("{base}/api/generate"))
.json(&serde_json::json!({
"model": model,
"prompt": prompt,
"stream": false,
}))
.timeout(std::time::Duration::from_secs(30))
.send()
.await?
.error_for_status()?
.json()
.await?;
let diag = resp.response.trim().to_string();
if diag.is_empty() {
return Ok(None);
}
Ok(Some(diag))
}
pub fn short_hint(diag: &str) -> String {
let first = diag.split('\n').next().unwrap_or("").trim();
let mut hint = first.to_string();
if hint.chars().count() > 200 {
hint.truncate(200);
hint.push('…');
}
hint
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_hint_takes_first_line_and_truncates() {
let d = "This is the first line.\nSecond line should be dropped.";
assert_eq!(short_hint(d), "This is the first line.");
let long = "x".repeat(300);
assert_eq!(short_hint(&long).chars().count(), 201);
}
}