pub use webfetch_core::{compress, http, refs, tls};
pub mod extract;
pub mod providers;
pub mod types;
use crate::compress::estimate_tokens;
pub use providers::Provider;
use types::{Reference, SearchOptions, SearchOutput, SearchResult, SearchStatus};
pub fn build_refs(results: &[SearchResult]) -> Vec<Reference> {
results
.iter()
.map(|r| Reference {
index: r.ref_index,
url: r.url.clone(),
})
.collect()
}
pub fn format_results(results: &[SearchResult]) -> String {
results
.iter()
.map(|r| {
if r.snippet.is_empty() {
format!("{} [{}]", r.title, r.ref_index)
} else {
format!("{} [{}]\n{}", r.title, r.ref_index, r.snippet)
}
})
.collect::<Vec<_>>()
.join("\n\n")
}
pub fn render_references(refs: &[Reference]) -> String {
crate::refs::render_block(refs)
}
pub fn render_output(output: &SearchOutput) -> String {
let mut s = format_results(&output.results);
let refs = render_references(&output.references);
if !refs.is_empty() {
s.push_str(&format!("\n\n{refs}"));
}
if let Some(note) = status_note(output) {
if !s.is_empty() {
s.push_str("\n\n");
}
s.push_str(¬e);
}
s
}
pub fn status_note(output: &SearchOutput) -> Option<String> {
match output.status {
SearchStatus::Ok => None,
SearchStatus::Empty => Some(format!(
"No results for `{}` (provider: {}).",
output.query, output.provider
)),
SearchStatus::Blocked => Some(format!(
"Search was blocked or returned an unrecognized page (provider: {}). \
This is not the same as having no results — the query was not answered.",
output.provider
)),
}
}
pub fn build_output(
query: &str,
results: Vec<SearchResult>,
status: SearchStatus,
provider: &str,
) -> SearchOutput {
let references = build_refs(&results);
let body = format_results(&results);
let refs_block = render_references(&references);
let full = if refs_block.is_empty() {
body
} else {
format!("{body}\n\n{refs_block}")
};
SearchOutput {
query: query.to_string(),
token_estimate: estimate_tokens(&full),
result_count: results.len(),
status,
provider: provider.to_string(),
references,
results,
}
}
pub fn build_output_from_ddg(query: &str, html: &str, max_results: usize) -> SearchOutput {
let results = extract::parse_ddg_lite(html, max_results);
let status = extract::classify_page(html, results.len());
build_output(query, results, status, "duckduckgo")
}
pub async fn run_search(options: SearchOptions) -> anyhow::Result<SearchOutput> {
let primary = attempt_provider(&options.provider, &options).await;
let primary_answered = matches!(&primary, Ok(out) if !out.status.is_failure());
if primary_answered {
return primary;
}
if let Some(fallback) = &options.fallback {
if let Ok(out) = attempt_provider(fallback, &options).await {
if !out.status.is_failure() {
return Ok(out);
}
}
}
primary
}
async fn attempt_provider(
provider: &Provider,
options: &SearchOptions,
) -> anyhow::Result<SearchOutput> {
let (results, status) = provider.search(options).await?;
Ok(build_output(
&options.query,
results,
status,
provider.label(),
))
}