diff --git a/src/client.rs b/src/client.rs index 5ebedb80..d5cf476e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1161,6 +1161,101 @@ pub async fn fetch_biorxiv(doi: &str) -> Result> { Ok(body.collection.into_iter().last()) } +/// Search You.com web search API for current research and broader context. +/// Returns results mapped to [`LitHit`] format for consistency with other sources. +pub async fn search_youcom(query: &str, limit: u32) -> Result> { + let api_key = std::env::var("YDC_API_KEY").ok(); + + // Use keyless API if no API key is available + let url = if api_key.is_some() { + "https://api.you.com/v1/search" + } else { + "https://api.you.com/v1/agents/search" + }; + + let payload = if api_key.is_some() { + serde_json::json!({ + "query": query, + "count": limit + }) + } else { + serde_json::json!({ + "query": query, + "count": limit + }) + }; + + let client = http(); + let mut req = client + .post(url) + .header("Content-Type", "application/json") + .header("User-Agent", ALPHAXIV_UA) + .json(&payload); + + if let Some(key) = api_key { + req = req.header("Authorization", format!("Bearer {}", key)); + } + + let res = req + .send() + .await + .map_err(|e| anyhow!("Could not reach You.com API: {}", e))?; + + let status = res.status(); + if !status.is_success() { + let error_text = res.text().await.unwrap_or_default(); + return Err(anyhow!( + "You.com API error ({} {}): {}", + status.as_u16(), + status.canonical_reason().unwrap_or(""), + error_text + )); + } + + #[derive(serde::Deserialize)] + struct YouComResult { + title: String, + url: String, + #[serde(alias = "snippet")] + description: String, + } + + #[derive(serde::Deserialize, Default)] + struct YouComWebResults { + #[serde(default)] + web: Vec, + } + + #[derive(serde::Deserialize)] + struct YouComResponse { + #[serde(default)] + results: YouComWebResults, + #[serde(default)] + web: Vec, // fallback for direct web results + } + + let body = res.json::().await?; + let results = if !body.results.web.is_empty() { + body.results.web + } else { + body.web + }; + + Ok(results + .into_iter() + .map(|result| LitHit { + source: "youcom".to_string(), + id: result.url.clone(), // Use URL as id for web results + title: result.title, + abstract_: result.description, + publication_date: None, // Web results don't have publication dates + votes: None, + citations: None, + snippets: vec![], // No text snippets for web results + }) + .collect()) +} + #[cfg(test)] mod tests { use super::{ diff --git a/src/commands/discover.rs b/src/commands/discover.rs index 4c31d589..f135a359 100644 --- a/src/commands/discover.rs +++ b/src/commands/discover.rs @@ -1,8 +1,8 @@ //! Independent literature retrieval primitives for the main agent. use crate::client::{ - discover_openalex, discover_papers_by_embedding, discover_papers_by_keyword, LitHit, - OpenAlexDiscoveryOptions, PaperDiscoveryOptions, BIORXIV_SOURCE_ID, + discover_openalex, discover_papers_by_embedding, discover_papers_by_keyword, search_youcom, + LitHit, OpenAlexDiscoveryOptions, PaperDiscoveryOptions, BIORXIV_SOURCE_ID, }; use crate::error::{anyhow, Result}; use crate::LitSource; @@ -40,6 +40,10 @@ pub async fn run(args: crate::DiscoverArgs) -> Result<()> { ) .await? } + crate::DiscoverCommand::Youcom(args) => { + ensure_source_enabled(LitSource::Youcom, &disabled)?; + search_youcom(&args.query, args.limit).await? + } }; println!("{}", serde_json::to_string_pretty(&results)?); diff --git a/src/commands/paper.rs b/src/commands/paper.rs index a73637e6..4acaf856 100644 --- a/src/commands/paper.rs +++ b/src/commands/paper.rs @@ -28,6 +28,7 @@ pub async fn run(args: crate::PaperArgs) -> Result<()> { LitSource::Alphaxiv => run_alphaxiv(&args).await, LitSource::Openalex => run_openalex(&args.id, args.full).await, LitSource::Biorxiv => run_biorxiv(&args.id, args.full).await, + LitSource::Youcom => run_youcom(&args.id), } } @@ -108,6 +109,18 @@ async fn run_biorxiv(raw: &str, full: bool) -> Result<()> { } } +fn run_youcom(url: &str) -> Result<()> { + // For You.com web search results, the "id" is actually a URL + // Since these are web pages, not academic papers, we just provide the URL + println!("# Web Search Result"); + println!("**URL:** {url}"); + println!(); + println!("This is a web search result from You.com. To view the content, please visit the URL above."); + println!(); + println!("To search for more related content, use: `orx lit --source youcom \"\"`"); + Ok(()) +} + fn print_openalex(w: &OpenAlexWork, full: bool) { if let Some(t) = &w.title { println!("# {t}"); diff --git a/src/main.rs b/src/main.rs index 99ce68f8..c8b4c5ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -603,6 +603,9 @@ pub enum LitSource { Openalex, /// bioRxiv biology preprints (searched via OpenAlex, fetched via bioRxiv). Biorxiv, + /// You.com web search for current research and broader context (opt-in; + /// enabled only when selected explicitly, no default fallback). + Youcom, } impl LitSource { @@ -614,6 +617,7 @@ impl LitSource { LitSource::Alphaxiv => "alphaxiv", LitSource::Openalex => "openalex", LitSource::Biorxiv => "biorxiv", + LitSource::Youcom => "youcom", } } @@ -623,6 +627,7 @@ impl LitSource { LitSource::Alphaxiv => "alphaXiv", LitSource::Openalex => "OpenAlex", LitSource::Biorxiv => "bioRxiv", + LitSource::Youcom => "You.com", } } } @@ -643,6 +648,10 @@ pub enum DiscoverCommand { Openalex(DiscoverySearchArgs), /// bioRxiv preprint search through OpenAlex's bioRxiv source index. Biorxiv(DiscoverySearchArgs), + /// You.com web search for current research and broader context. Opt-in; + /// requires `YDC_API_KEY` for the authenticated endpoint, falls back to + /// the keyless agents endpoint otherwise. + Youcom(DiscoverySearchArgs), } #[derive(Args, Debug)]