Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,101 @@ pub async fn fetch_biorxiv(doi: &str) -> Result<Option<BiorxivDetail>> {
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<Vec<LitHit>> {
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<YouComResult>,
}

#[derive(serde::Deserialize)]
struct YouComResponse {
#[serde(default)]
results: YouComWebResults,
#[serde(default)]
web: Vec<YouComResult>, // fallback for direct web results
}

let body = res.json::<YouComResponse>().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::{
Expand Down
8 changes: 6 additions & 2 deletions src/commands/discover.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)?);
Expand Down
13 changes: 13 additions & 0 deletions src/commands/paper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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 \"<your query>\"`");
Ok(())
}

fn print_openalex(w: &OpenAlexWork, full: bool) {
if let Some(t) = &w.title {
println!("# {t}");
Expand Down
9 changes: 9 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -614,6 +617,7 @@ impl LitSource {
LitSource::Alphaxiv => "alphaxiv",
LitSource::Openalex => "openalex",
LitSource::Biorxiv => "biorxiv",
LitSource::Youcom => "youcom",
}
}

Expand All @@ -623,6 +627,7 @@ impl LitSource {
LitSource::Alphaxiv => "alphaXiv",
LitSource::Openalex => "OpenAlex",
LitSource::Biorxiv => "bioRxiv",
LitSource::Youcom => "You.com",
}
}
}
Expand All @@ -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)]
Expand Down