//! Asking TypeSafe's Jev which Apple category a feed belongs to. use crate::parse::ParsedFeed; use crate::{category, fetch}; use anyhow::{Result, anyhow}; /// TypeSafe's System One endpoint, where Jev answers questions about a piece of state. pub const JEV_URL: &str = "https://api.typesafe.ai/v1/systemone"; /// A Directory category for a feed that names none of its own, which most blogs do not (#117): /// TypeSafe's Jev picks one of Apple's categories or subcategories from the feed's title and /// latest items, the subcategory being what a podcast's own category is stored as. The likeliest /// is taken however unsure, since an admin can change it and asking again would cost a call a scan. pub async fn classify(client: &reqwest::Client, key: &str, feed: &ParsedFeed) -> Result { let items: Vec<&str> = feed.entries.iter().filter_map(|e| e.title.as_deref()).take(15).collect(); // A subcategory says which category it is in; without that, "Games" or "Drama" is ambiguous. let mut criteria = serde_json::Map::new(); for (category, subs) in category::CATEGORIES { criteria.insert(category.to_string(), serde_json::Value::Null); for sub in *subs { criteria.insert(sub.to_string(), format!("Within {category}").into()); } } let body = serde_json::json!({ "model": "jev-latest", "state": { "title": feed.title, "recent_items": items }, "questions": { "category": { "type": "choice", "instructions": "Which category best describes what this feed publishes?", "criteria": criteria, }}, }); let answer: serde_json::Value = client .post(JEV_URL) .bearer_auth(key) .json(&body) .timeout(fetch::FEED_TIMEOUT) .send() .await? .error_for_status()? .json() .await?; picked(&answer) } pub(crate) fn picked(answer: &serde_json::Value) -> Result { let choice = answer["answers"]["category"]["choice"].as_str().unwrap_or_default(); category::CATEGORIES .iter() .flat_map(|(c, subs)| std::iter::once(c).chain(subs.iter())) .find(|c| **c == choice) .map(|c| c.to_string()) .ok_or_else(|| anyhow!("no category in the answer: {answer}")) } #[cfg(test)] mod tests { #[test] fn picked_takes_only_an_apple_category() { let answer = |c: &str| serde_json::json!({"answers": {"category": {"type": "choice", "choice": c}}}); assert_eq!(super::picked(&answer("Technology")).unwrap(), "Technology"); assert_eq!(super::picked(&answer("Tech News")).unwrap(), "Tech News", "a subcategory"); assert!(super::picked(&answer("Blogs")).is_err(), "not one of Apple's"); assert!(super::picked(&serde_json::json!({"error": "rate limited"})).is_err()); } }