use anyhow::{Context, Result};
use reqwest::Client;
use serde::Deserialize;
use std::fs;
#[derive(Deserialize)]
struct GeminiResponse {
candidates: Vec<Candidate>,
}
#[derive(Deserialize)]
struct Candidate {
content: Content,
}
#[derive(Deserialize)]
struct Content {
parts: Vec<Part>,
}
#[derive(Deserialize)]
struct Part {
text: Option<String>,
}
pub async fn generate_report(
analysis: &crate::results::Analysis,
api_key: &str,
client: &Client,
) -> Result<String> {
let summary_json = serde_json::to_value(analysis)?;
let base_prompt_template = fs::read_to_string("src/config/prompt.txt")
.context("Failed to read prompt file")?;
let instruction = base_prompt_template
.replace("__JSON_DATA_PLACEHOLDER__", &summary_json.to_string());
let url = format!(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}"
);
let body = serde_json::json!({
"contents": [{
"parts": [{ "text": instruction }],
"role": "user"
}]
});
let resp = client
.post(&url)
.json(&body)
.send()
.await
.context("Failed to call Gemini")?
.error_for_status()
.context("Gemini returned HTTP error")?;
let resp_json: GeminiResponse = resp.json().await.context("Invalid JSON")?;
let answer = resp_json
.candidates
.first()
.and_then(|c| c.content.parts.first())
.and_then(|p| p.text.as_ref())
.cloned()
.unwrap_or_else(|| "Gemini returned an empty response.".to_string());
Ok(answer)
}